diff --git a/admin-compliance/app/sdk/onboarding-advisor/page.tsx b/admin-compliance/app/sdk/onboarding-advisor/page.tsx new file mode 100644 index 00000000..a3932e43 --- /dev/null +++ b/admin-compliance/app/sdk/onboarding-advisor/page.tsx @@ -0,0 +1,200 @@ +'use client' + +// ETO / Onboarding-Advisor — thin operator surface over POST /api/compliance/onboarding/advisor-start. +// Certifications + target + scanner findings -> Silent Pass -> Advisor. NOT the regulation gap engine +// (/sdk/gap-analysis is a different flow: product -> applicable regulations). This tests the cert->delta +// case: "TISAX/ISO27001 -> CRA, what is auto-detected, what stays an open question?". No new backend. + +import React, { useEffect, useState } from 'react' + +const CERTS = ['ISO27001', 'TISAX', 'ISO9001', 'IEC62443', 'ISO13485', 'ISO14001', 'ASPICE', 'IATF16949'] + +// label -> {signal_id, source_type} — demonstrates all three signal KINDS (observation / partial / requirement) +const FINDINGS: Array<{ label: string; signal_id: string; source_type: string; kind: string }> = [ + { label: 'SBOM im Repo (CycloneDX/SPDX)', signal_id: 'cyclonedx_found', source_type: 'repository', kind: 'observation' }, + { label: 'security.txt / CVD-Policy veröffentlicht', signal_id: 'security_txt', source_type: 'website', kind: 'observation' }, + { label: 'Signierte Releases', signal_id: 'signed_releases', source_type: 'repository', kind: 'observation' }, + { label: 'Produkt-Risikobewertung (Dokument)', signal_id: 'risk_assessment_pdf', source_type: 'document', kind: 'observation' }, + { label: 'CI-Pipeline vorhanden (nur Indikation)', signal_id: 'github_actions_ci', source_type: 'repository', kind: 'partial' }, + { label: 'Cloud-/vernetztes Produkt', signal_id: 'cloud_hosted', source_type: 'product', kind: 'observation' }, + { label: 'Ausschreibung FORDERT SBOM (Requirement)', signal_id: 'requires_sbom', source_type: 'tender', kind: 'requirement' }, + { label: 'OEM FORDERT PSIRT (Requirement)', signal_id: 'supplier_requires_psirt', source_type: 'oem', kind: 'requirement' }, +] + +interface Question { capability_id: string; question_intent: string; why: string; information_value: number; priority: string } +interface Inferred { certification: string; capabilities: string[]; statement: string } +interface Rejected { certification?: string; statement: string; reason: string } +interface Measure { capability_id: string; leverage: number; closes: string[] } +interface AdvisorResponse { + silent_intake_summary: string; headline: string; auto_detected: string[]; indications: string[] + inferred_assumptions: Inferred[]; rejected_assumptions: Rejected[]; top_5_questions: Question[] + capability_delta: string[]; top_measures: Measure[]; evidence_requests: string[] + unsupported_domains: string[]; completeness_summary: string; capability_labels: Record +} + +const PROXY = '/api/sdk/v1/compliance/onboarding' + +function Chips({ items, tone }: { items: string[]; tone: string }) { + if (!items.length) return + return ( +
+ {items.map(c => {c})} +
+ ) +} + +function Section({ title, hint, children }: { title: string; hint?: string; children: React.ReactNode }) { + return ( +
+

{title}

+ {hint &&

{hint}

} +
{children}
+
+ ) +} + +export default function OnboardingAdvisorPage() { + const [targets, setTargets] = useState([]) + const [company, setCompany] = useState('Beispiel Maschinenbau') + const [industry, setIndustry] = useState('machine_builder') + const [certs, setCerts] = useState(['ISO27001', 'ISO9001']) + const [target, setTarget] = useState('CRA') + const [findings, setFindings] = useState(['cyclonedx_found', 'github_actions_ci', 'requires_sbom']) + const [knownEvidence, setKnownEvidence] = useState('CE-Prozess') + const [result, setResult] = useState(null) + const [loading, setLoading] = useState(false) + const [error, setError] = useState('') + + useEffect(() => { + fetch(`${PROXY}/targets`).then(r => r.json()).then(d => { + if (Array.isArray(d.targets)) { setTargets(d.targets); if (!d.targets.includes('CRA') && d.targets[0]) setTarget(d.targets[0]) } + }).catch(() => {}) + }, []) + + const toggle = (list: string[], set: (v: string[]) => void, v: string) => + set(list.includes(v) ? list.filter(x => x !== v) : [...list, v]) + + const lbl = (id: string) => result?.capability_labels?.[id] || id.replace(/_/g, ' ') + + const run = async () => { + setLoading(true); setError(''); setResult(null) + try { + const scanner_findings = FINDINGS.filter(f => findings.includes(f.signal_id)) + .map(f => ({ signal_id: f.signal_id, source_type: f.source_type })) + const res = await fetch(`${PROXY}/advisor-start`, { + method: 'POST', headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + company, industry, products: [], markets: ['EU'], certifications: certs, + known_evidence: knownEvidence ? knownEvidence.split(',').map(s => s.trim()).filter(Boolean) : [], + target, scanner_findings, + }), + }) + if (!res.ok) throw new Error(await res.text()) + setResult(await res.json()) + } catch (e) { + setError(e instanceof Error ? e.message : 'Advisor fehlgeschlagen') + } finally { setLoading(false) } + } + + // auto-recompute when certifications / target / scanner signals change (no button click needed) + useEffect(() => { if (certs.length) run() }, [certs, target, findings]) // eslint-disable-line react-hooks/exhaustive-deps + + return ( +
+
+

ETO / Onboarding-Advisor

+

+ Zertifikate + Ziel + Scanner-Signale → Silent Pass → Capability-Delta + nächste beste Fragen. + Welt-1: ein Zertifikat legt nahe, beweist nichts (Verifikation erforderlich). +

+ +
+
+ + + + +
+ +
+
+ {CERTS.map(c => ( + + ))} +
+
+
+ +
+
+ {FINDINGS.map(f => ( + + ))} +
+
+ + + + {error &&
{error}
} + + {result && ( +
+
+
{result.headline}
+
{result.silent_intake_summary}
+
+
+
+
+
+
+ {result.top_5_questions.length ? ( +
    + {result.top_5_questions.map((q, i) => ( +
  1. +
    {i + 1}. {lbl(q.capability_id)}
    +
    {q.why}
    +
  2. + ))} +
+ ) : } +
+
+
+ {result.inferred_assumptions.length ? result.inferred_assumptions.map(a => ( +
{a.certification}: {a.capabilities.map(lbl).join(', ')}
+ )) : } +
+
+ {result.rejected_assumptions.length ? result.rejected_assumptions.map((a, i) => ( +
{a.statement}
+ )) : } +
+
+
+
+
+
+
+ {result.completeness_summary || '—'} +
+
+ )} +
+
+ ) +} diff --git a/admin-compliance/lib/sdk/agents/__tests__/advisor-rag.test.ts b/admin-compliance/lib/sdk/agents/__tests__/advisor-rag.test.ts index 54fb8fdb..007bfb80 100644 --- a/admin-compliance/lib/sdk/agents/__tests__/advisor-rag.test.ts +++ b/admin-compliance/lib/sdk/agents/__tests__/advisor-rag.test.ts @@ -51,8 +51,8 @@ describe('advisor-rag', () => { }) }) - describe('queryAdvisorRAG', () => { - it('fragt alle 6 Collections ab und formatiert die Treffer', async () => { + describe('queryAdvisorRAG (Authority Router)', () => { + it('ruft den Router EINMAL auf und formatiert die Treffer', async () => { mockFetch.mockResolvedValue({ ok: true, json: async () => ({ results: [{ text: 'Inhalt A', regulation_short: 'DSGVO', score: 0.9 }] }), @@ -60,19 +60,19 @@ describe('advisor-rag', () => { const result = await mod.queryAdvisorRAG('Was ist eine DSFA?') expect(result).toContain('[Quelle 1: DSGVO]') expect(result).toContain('Inhalt A') - expect(mockFetch).toHaveBeenCalledTimes(mod.COMPLIANCE_COLLECTIONS.length) + expect(mockFetch).toHaveBeenCalledTimes(1) }) - it('ruft die ai-sdk /sdk/v1/rag/search mit collection + top_k auf', async () => { + it('ruft /sdk/v1/rag/retrieve mit query + top_k (ohne collection) auf', async () => { mockFetch.mockResolvedValue({ ok: true, json: async () => ({ results: [] }) }) await mod.queryAdvisorRAG('test') expect(mockFetch).toHaveBeenCalledWith( - expect.stringContaining('/sdk/v1/rag/search'), + expect.stringContaining('/sdk/v1/rag/retrieve'), expect.objectContaining({ method: 'POST' }), ) const body = JSON.parse(mockFetch.mock.calls[0][1].body) - expect(body).toMatchObject({ query: 'test', top_k: 3 }) - expect(mod.COMPLIANCE_COLLECTIONS).toContain(body.collection) + expect(body).toMatchObject({ query: 'test', top_k: 8 }) + expect(body.collection).toBeUndefined() }) it('liefert leeren String wenn das RAG-Backend nicht erreichbar ist (graceful)', async () => { @@ -80,10 +80,5 @@ describe('advisor-rag', () => { const result = await mod.queryAdvisorRAG('test') expect(result).toBe('') }) - - it('umfasst genau die 6 Compliance-Collections', () => { - expect(mod.COMPLIANCE_COLLECTIONS).toHaveLength(6) - expect(mod.COMPLIANCE_COLLECTIONS).toContain('bp_compliance_recht') - }) }) }) diff --git a/admin-compliance/lib/sdk/agents/advisor-rag.ts b/admin-compliance/lib/sdk/agents/advisor-rag.ts index 1eef3648..b42d1cd2 100644 --- a/admin-compliance/lib/sdk/agents/advisor-rag.ts +++ b/admin-compliance/lib/sdk/agents/advisor-rag.ts @@ -1,12 +1,13 @@ /** * Compliance-Advisor RAG-Suche. * - * Fragt die ai-compliance-sdk (`/sdk/v1/rag/search`) ab statt des frueheren - * `rag-service:8097` (auf prod nicht erreichbar). Die ai-sdk embeddet die Query - * mit bge-m3 (prod: ollama-embed) und sucht in den Qdrant-Compliance-Collections - * — damit profitiert der Advisor vom reicheren Embedding. + * Fragt den Authority Router der ai-compliance-sdk (`/sdk/v1/rag/retrieve`) mit NUR der + * Query ab — der Router waehlt selbst die Collections (Broad-Authority-Base + KB-2026.1-Slice + * bei in-scope), embeddet mit bge-m3 (prod: ollama-embed), merged + authority-ranked. Der + * Advisor bleibt damit collection-agnostisch (Vertrag: Compiler -> Collections -> Retriever + * -> Advisor); die fruehere Multi-Collection-Logik liegt jetzt im Retriever. * - * Fehler je Collection werden geschluckt (graceful: Antwort ohne diesen Treffer). + * Fehler werden geschluckt (graceful: Antwort ohne RAG-Kontext). * Fundstellen via article_label sind live ab dem Prod-Re-Ingest 2026-06. */ @@ -17,16 +18,6 @@ const DEFAULT_USER = '00000000-0000-0000-0000-000000000001' const DEFAULT_TENANT = process.env.DEFAULT_TENANT_ID || '9282a473-5c95-4b3a-bf78-0ecc0ec71d3e' -// Compliance-relevante Collections (ai-sdk-Whitelist `AllowedCollections`). -export const COMPLIANCE_COLLECTIONS = [ - 'bp_compliance_gesetze', - 'bp_compliance_ce', - 'bp_compliance_datenschutz', - 'bp_dsfa_corpus', - 'bp_compliance_recht', - 'bp_legal_templates', -] as const - interface SdkRagResult { text?: string regulation_code?: string @@ -68,39 +59,36 @@ export function mapSdkResults(results: SdkRagResult[] | undefined): ScoredPassag .filter((p) => p.content) } -async function searchCollection(collection: string, query: string): Promise { +/** + * Authority Router: EIN collection-agnostischer Aufruf an die ai-sdk (`/sdk/v1/rag/retrieve`). + * Der Router waehlt die Collections (Broad-Authority-Base + KB-2026.1-Slice bei in-scope), + * merged + authority-ranked sie und liefert die Top-Passagen. Der Advisor weiss damit nichts + * mehr ueber einzelne Collections — die fruehere Multi-Collection-Logik liegt jetzt im Retriever. + * Fehler werden geschluckt (graceful: Antwort ohne RAG-Kontext). + */ +export async function queryAdvisorRAG(query: string): Promise { + let passages: ScoredPassage[] = [] try { - const res = await fetch(`${SDK_URL}/sdk/v1/rag/search`, { + const res = await fetch(`${SDK_URL}/sdk/v1/rag/retrieve`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-User-ID': DEFAULT_USER, 'X-Tenant-ID': DEFAULT_TENANT, }, - body: JSON.stringify({ query, collection, top_k: 3 }), - signal: AbortSignal.timeout(10000), + body: JSON.stringify({ query, top_k: 8 }), + signal: AbortSignal.timeout(15000), }) - if (!res.ok) return [] - const data = await res.json() - return mapSdkResults(data.results) + if (res.ok) { + const data = await res.json() + passages = mapSdkResults(data.results) + } } catch { - return [] + // graceful: keine Verbindung -> Antwort ohne RAG-Kontext } -} - -/** - * Fragt alle Compliance-Collections parallel ab und liefert die Top-8-Passagen - * als formatierten Kontextblock (oder '' wenn nichts erreichbar/gefunden). - */ -export async function queryAdvisorRAG(query: string): Promise { - const settled = await Promise.all( - COMPLIANCE_COLLECTIONS.map((c) => searchCollection(c, query)), - ) - const all = settled.flat() - if (all.length === 0) return '' - all.sort((a, b) => b.score - a.score) - return all - .slice(0, 8) + // Der Router liefert bereits authority-geordnete Top-K; Reihenfolge bewahren. + if (passages.length === 0) return '' + return passages .map((r, i) => `[Quelle ${i + 1}: ${r.source}]\n${r.content}`) .join('\n\n---\n\n') } diff --git a/ai-compliance-sdk/Dockerfile b/ai-compliance-sdk/Dockerfile index b87ea258..b1588140 100644 --- a/ai-compliance-sdk/Dockerfile +++ b/ai-compliance-sdk/Dockerfile @@ -1,4 +1,6 @@ # Build stage +# ci-retrigger 2026-06-27: transient registry.meghsakha.com 502 on push (Runde 1) + last-build +# tag-bug skipped the rerun (Runde 2). No logic change — forces detect-changes to rebuild ai-sdk. FROM golang:1.24-alpine AS builder WORKDIR /app diff --git a/ai-compliance-sdk/internal/api/handlers/rag_handlers.go b/ai-compliance-sdk/internal/api/handlers/rag_handlers.go index eac9e17a..1842c43b 100644 --- a/ai-compliance-sdk/internal/api/handlers/rag_handlers.go +++ b/ai-compliance-sdk/internal/api/handlers/rag_handlers.go @@ -82,6 +82,43 @@ func (h *RAGHandlers) Search(c *gin.Context) { }) } +// RetrieveRequest is the Authority Router request: a query only, no collection — the router decides +// which collections to query (broad authority base + the in-scope KB-2026.1 slice). +type RetrieveRequest struct { + Query string `json:"query" binding:"required"` + TopK int `json:"top_k,omitempty"` +} + +// Retrieve is the Authority Router endpoint. The Advisor calls this with ONLY a query and stays +// collection-agnostic; the router fans out over the authority base + the in-scope slice, merges by +// authority score, and returns the unified top-K. Response shape matches Search (query/results/ +// count/assessment) so existing consumers parse it unchanged. +// POST /sdk/v1/rag/retrieve +func (h *RAGHandlers) Retrieve(c *gin.Context) { + var req RetrieveRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + if req.TopK <= 0 || req.TopK > 20 { + req.TopK = 8 + } + + results, err := h.ragClient.Retrieve(c.Request.Context(), req.Query, req.TopK) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "RAG retrieve failed: " + err.Error()}) + return + } + + c.JSON(http.StatusOK, gin.H{ + "query": req.Query, + "results": results, + "count": len(results), + "assessment": ucca.Assess(results), + }) +} + // ListRegulations returns the list of available regulations in the corpus. // GET /sdk/v1/rag/regulations func (h *RAGHandlers) ListRegulations(c *gin.Context) { diff --git a/ai-compliance-sdk/internal/app/routes.go b/ai-compliance-sdk/internal/app/routes.go index 562f956b..1f8d6fb0 100644 --- a/ai-compliance-sdk/internal/app/routes.go +++ b/ai-compliance-sdk/internal/app/routes.go @@ -159,6 +159,7 @@ func registerRAGRoutes(v1 *gin.RouterGroup, h *handlers.RAGHandlers) { ragRoutes := v1.Group("/rag") { ragRoutes.POST("/search", h.Search) + ragRoutes.POST("/retrieve", h.Retrieve) ragRoutes.GET("/regulations", h.ListRegulations) ragRoutes.GET("/corpus-status", h.CorpusStatus) ragRoutes.GET("/corpus-versions/:collection", h.CorpusVersionHistory) @@ -358,7 +359,6 @@ func registerWhistleblowerRoutes(v1 *gin.RouterGroup, h *handlers.WhistleblowerH } } - func registerMaximizerRoutes(v1 *gin.RouterGroup, h *handlers.MaximizerHandlers) { m := v1.Group("/maximizer") { diff --git a/ai-compliance-sdk/internal/iace/proposer_pin.go b/ai-compliance-sdk/internal/iace/proposer_pin.go new file mode 100644 index 00000000..e883c6e9 --- /dev/null +++ b/ai-compliance-sdk/internal/iace/proposer_pin.go @@ -0,0 +1,73 @@ +package iace + +// P3: pin accepted proposer decisions into the GT gate. +// +// When a human accepts a proposal from the offline proposer (a dedup +// supersession, a foreign-framing gate, a vocab→tag mapping, a coverage hazard), +// they record an AcceptedPin. A pin is a tiny, machine-scoped invariant — "this +// pattern MUST (or must NOT) fire for this machine" — that a test re-checks on +// every run. This is what makes the library's growth COMPOUND into the gate +// instead of silently eroding it: a future change that re-introduces a dropped +// duplicate, un-gates a foreign pattern, or removes a coverage hazard breaks the +// pin and fails CI. +// +// A single boolean covers all four proposal types: +// - dedup supersession accepted → DropPattern MustFire=false +// - foreign-framing gate accepted → foreign pattern MustFire=false +// - vocab→tag / coverage hazard accepted → the enabled pattern MustFire=true + +// AcceptedPin is one regression invariant for an accepted proposal. +type AcceptedPin struct { + Pattern string `json:"pattern"` + MustFire bool `json:"must_fire"` + Reason string `json:"reason"` + FromProposal string `json:"from_proposal,omitempty"` +} + +// PinSet is the accepted-pin registry for one machine (testdata/accepted_pins_*.json). +type PinSet struct { + Machine string `json:"machine"` + Pins []AcceptedPin `json:"pins"` +} + +// PinResult is the verdict for one pin against an engine run. +type PinResult struct { + Pin AcceptedPin + OK bool + Detail string +} + +// VerifyPins checks every pin against the set of pattern IDs the engine actually +// fired for the machine. A pin holds iff the pattern's presence equals MustFire. +func VerifyPins(pins []AcceptedPin, firedPatternIDs []string) []PinResult { + fired := make(map[string]bool, len(firedPatternIDs)) + for _, id := range firedPatternIDs { + fired[id] = true + } + out := make([]PinResult, 0, len(pins)) + for _, p := range pins { + got := fired[p.Pattern] + ok := got == p.MustFire + detail := "ok" + if !ok { + if p.MustFire { + detail = "expected to fire but did NOT — coverage/mapping regressed" + } else { + detail = "expected to be suppressed but FIRED — gate/supersession regressed" + } + } + out = append(out, PinResult{Pin: p, OK: ok, Detail: detail}) + } + return out +} + +// GenerateDedupPin turns an accepted (verdict=duplicate) dedup candidate into the +// pin that protects the supersession: the dropped pattern must no longer fire. +func GenerateDedupPin(c DedupCandidate) AcceptedPin { + return AcceptedPin{ + Pattern: c.DropPattern, + MustFire: false, + Reason: "accepted duplicate of " + c.KeepPattern + " (" + c.Category + ")", + FromProposal: "dedup " + c.DropPattern + " -> " + c.KeepPattern, + } +} diff --git a/ai-compliance-sdk/internal/iace/proposer_pin_test.go b/ai-compliance-sdk/internal/iace/proposer_pin_test.go new file mode 100644 index 00000000..f2b0bbab --- /dev/null +++ b/ai-compliance-sdk/internal/iace/proposer_pin_test.go @@ -0,0 +1,63 @@ +package iace + +import ( + "encoding/json" + "os" + "path/filepath" + "testing" +) + +func TestVerifyPins(t *testing.T) { + pins := []AcceptedPin{ + {Pattern: "HPa", MustFire: true}, + {Pattern: "HPb", MustFire: false}, + } + res := VerifyPins(pins, []string{"HPa", "HPb"}) + if !res[0].OK { + t.Errorf("HPa must_fire=true and it fired -> should be OK") + } + if res[1].OK { + t.Errorf("HPb must_fire=false but it fired -> should be VIOLATED") + } + res2 := VerifyPins(pins, []string{}) + if res2[0].OK || !res2[1].OK { + t.Errorf("expected HPa violated + HPb ok, got %+v", res2) + } +} + +func TestGenerateDedupPin(t *testing.T) { + pin := GenerateDedupPin(DedupCandidate{KeepPattern: "HP144", DropPattern: "HP013", Category: "electrical_hazard"}) + if pin.Pattern != "HP013" || pin.MustFire { + t.Fatalf("want pin {HP013, must_fire=false}, got %+v", pin) + } +} + +// TestWarewashing_AcceptedPins re-checks every accepted P1 supersession against the +// live warewashing engine output. A future change that un-suppresses HP013/016/018 +// or drops HP2201/HP144 breaks a pin here — the gate compounds, not erodes. +func TestWarewashing_AcceptedPins(t *testing.T) { + raw, err := os.ReadFile(filepath.Join("testdata", "accepted_pins_warewashing.json")) + if err != nil { + t.Fatalf("read pins: %v", err) + } + var ps PinSet + if err := json.Unmarshal(raw, &ps); err != nil { + t.Fatalf("parse pins: %v", err) + } + + _, _, kept := warewashingEngineOutput() + firedIDs := make([]string, 0, len(kept)) + for _, pm := range kept { + firedIDs = append(firedIDs, pm.PatternID) + } + + ok := 0 + for _, r := range VerifyPins(ps.Pins, firedIDs) { + if r.OK { + ok++ + continue + } + t.Errorf("PIN VIOLATED: %s (must_fire=%v) — %s [%s]", r.Pin.Pattern, r.Pin.MustFire, r.Detail, r.Pin.Reason) + } + t.Logf("accepted pins for %q: %d/%d hold", ps.Machine, ok, len(ps.Pins)) +} diff --git a/ai-compliance-sdk/internal/iace/testdata/accepted_pins_warewashing.json b/ai-compliance-sdk/internal/iace/testdata/accepted_pins_warewashing.json new file mode 100644 index 00000000..f013918f --- /dev/null +++ b/ai-compliance-sdk/internal/iace/testdata/accepted_pins_warewashing.json @@ -0,0 +1,10 @@ +{ + "machine": "Gewerbliche Untertisch-Geschirrspuelmaschine (vernetzt)", + "pins": [ + {"pattern": "HP016", "must_fire": false, "reason": "generic hot-surface (Formwerkzeuge/Auspuffleitung framing) superseded by HP2201", "from_proposal": "P1 thermal supersession"}, + {"pattern": "HP018", "must_fire": false, "reason": "actuator-burn superseded by HP2201", "from_proposal": "P1 thermal supersession"}, + {"pattern": "HP013", "must_fire": false, "reason": "stored-energy Batterie/USV framing superseded by HP144", "from_proposal": "P1 stored-energy supersession"}, + {"pattern": "HP2201", "must_fire": true, "reason": "warewashing hot-surface (Boiler/Tank/Spuelkammer) must remain — it is the clean equivalent that replaces HP016/HP018", "from_proposal": "P1 thermal supersession"}, + {"pattern": "HP144", "must_fire": true, "reason": "residual-voltage (Frequenzumrichter/Zwischenkreis) must remain — clean equivalent that replaces HP013", "from_proposal": "P1 stored-energy supersession"} + ] +} diff --git a/ai-compliance-sdk/internal/ucca/authority.go b/ai-compliance-sdk/internal/ucca/authority.go index 6627b57e..d80eb178 100644 --- a/ai-compliance-sdk/internal/ucca/authority.go +++ b/ai-compliance-sdk/internal/ucca/authority.go @@ -110,9 +110,10 @@ type domainDef struct { // Deterministic order (slice, not map) — important for stable classification + tests. var domains = []domainDef{ {"data_protection", - []string{"DSGVO", "GDPR", "BDSG", "EDPB", "DSK", "BfDI", "BayLfD", "DPF"}, + []string{"DSGVO", "GDPR", "BDSG", "TDDDG", "TTDSG", "EDPB", "DSK", "BfDI", "BayLfD", "DPF"}, []string{"personenbezogen", "betroffene", "datenschutz", "datenschutzbeauftrag", "dsb", - "datenpanne", "auskunft", "loesch", "lösch", "einwilligung", "besondere kategorien", "auftragsverarbeiter"}}, + "datenpanne", "auskunft", "loesch", "lösch", "einwilligung", "besondere kategorien", "auftragsverarbeit", + "cookie", "endeinrichtung", "endgerät", "endgeraet", "tracking"}}, {"cyber", []string{"CRA", "NIS2", "NIS-2", "ENISA", "DORA", "EUCC"}, []string{"security update", "sicherheitsupdate", "sicherheitsaktualisierung", "schwachstelle", "sbom", @@ -126,6 +127,16 @@ var domains = []domainDef{ nil}, } +// euPrimaryDomains are domains whose PRIMARY binding act is an EU regulation/directive +// (DSGVO, CRA/NIS2, AI Act, MaschinenVO). In these domains a NATIONAL implementing law +// (e.g. BDSG) is subsidiary for general questions — see nationalSubsidiarityPenalty. +var euPrimaryDomains = map[string]bool{ + "data_protection": true, + "cyber": true, + "ai": true, + "product_safety": true, +} + func queryDomain(query string) string { ql := strings.ToLower(query) for _, d := range domains { @@ -135,6 +146,16 @@ func queryDomain(query string) string { } } } + // Fallback: an explicit regulation mention (e.g. "DSGVO", "BDSG", "CRA") also signals the + // domain — so a question phrased around the act ("... gilt die DSGVO ...") is scoped even + // without a topical keyword. Keyword match wins first (more specific). + for _, d := range domains { + for _, reg := range d.regs { + if strings.Contains(ql, strings.ToLower(reg)) { + return d.name + } + } + } return "" } @@ -180,6 +201,11 @@ var topics = []topicDef{ {[]string{"bussgeld", "geldbusse"}, []string{"Art. 83"}}, {[]string{"security update", "sicherheitsupdate", "schwachstelle", "sbom", "cybersicherheitsanforderung"}, []string{"CRA Anhang I"}}, {[]string{"meldepflicht", "sicherheitsvorfall"}, []string{"Art. 14 CRA"}}, + // ePrivacy / cookies: § 25 TDDDG (ex-TTDSG) is lex specialis for terminal-equipment access / + // cookie consent. Co-primary on a cookie/tracking query, so the subsidiarity rule does NOT + // demote it like general-DP DE law subsidiary to the DSGVO. Keywords are cookie-specific + // (NOT bare "Einwilligung") so a general consent question still resolves to Art. 7 DSGVO. + {[]string{"cookie", "endeinrichtung", "endgerät", "endgeraet", "tracking", "speicherung von informationen", "zugriff auf informationen"}, []string{"§ 25 TDDDG"}}, } // resultMatchesTopic reports whether the result is a preferred norm of a topic the query hits. diff --git a/ai-compliance-sdk/internal/ucca/authority_rerank.go b/ai-compliance-sdk/internal/ucca/authority_rerank.go index 611b0111..e6a30b85 100644 --- a/ai-compliance-sdk/internal/ucca/authority_rerank.go +++ b/ai-compliance-sdk/internal/ucca/authority_rerank.go @@ -14,6 +14,7 @@ const ( domainMatchGain = 0.15 offDomainPenalty = 0.10 // off-domain binding (demoted, not removed) scopePenalty = 0.25 // BDSG Teil 3 (law enforcement) on a general DP question + subsidiarityPen = 0.18 // national implementing law (BDSG) on a general EU-primary question: SOFT demote, not exclusion topicGain = 0.18 // amplifier only supersededPenalty = 0.50 // superseded Alt-Quelle (pre-eu-v1): demoted, nicht versteckt intentLiftGain = 0.10 // epsilon a qualifying interpretative source is lifted ABOVE the best binding @@ -27,6 +28,10 @@ var guidanceIntentSignals = []string{ "edpb", "europäischer datenschutzausschuss", "europaeischer datenschutzausschuss", "dsk", "enisa", "bsi", "leitlinie", "guideline", "orientierungshilfe", "auslegung", "empfiehlt", "empfehlung", "sagt", "laut", + // Guidance-Dokumente direkt benannt (WP29-Working-Papers WP2xx + EDPB-Guidelines "GL 0x/20xx"): + // "Welche Kriterien nennt WP248 ..." / "Was sagt GL 07/2020 ..." tragen Guidance-Intent ohne + // die Verben oben. Fix: queryWantsGuidance verfehlte rein-doc-namige Formulierungen. + "wp2", "wp 2", "wp29", "working paper", "gl 0", } // controlIntentSignals mark a query that asks HOW to implement / which controls or @@ -102,6 +107,15 @@ func authorityScore(query string, r LegalSearchResult, qDomain string, qForeign if qDomain == "data_protection" && scopeClass(r) == "law_enforcement" { score -= scopePenalty } + // Subsidiarity: a national implementing law (DE binding, e.g. BDSG) is subsidiary to the + // primary EU act for GENERAL questions in an EU-primary domain — UNLESS the query hits a + // topic where the national norm is co-primary (DSB §38, special categories §22, ...). The + // topic boost below lifts those; here we only SOFT-demote the non-topic national norm, so + // it stays visible and can still win on a strongly matching topic. No hard exclusion. + if euPrimaryDomains[qDomain] && info.sourceClass == "binding_law" && + info.jurisdiction == "DE" && !resultMatchesTopic(query, r) { + score -= subsidiarityPen + } if resultMatchesTopic(query, r) { score += topicGain // Verstaerker, kein Override } diff --git a/ai-compliance-sdk/internal/ucca/authority_rerank_test.go b/ai-compliance-sdk/internal/ucca/authority_rerank_test.go index 65e5f16c..857b1a01 100644 --- a/ai-compliance-sdk/internal/ucca/authority_rerank_test.go +++ b/ai-compliance-sdk/internal/ucca/authority_rerank_test.go @@ -72,6 +72,95 @@ func TestRerankByAuthority_Acceptance(t *testing.T) { } }) + // Subsidiarity (KB-2026.1 BDSG-pilot regression): a national implementing § that is NOT a + // co-primary topic norm must not outrank the primary DSGVO article on a general question. + t.Run("subsidiarity dp_05: BDSG §23 below DSGVO Art.6 (Rechtsgrundlage)", func(t *testing.T) { + in := []LegalSearchResult{ + bindingRes("§ 23 BDSG", "BDSG", "DE", 0.70), + bindingRes("Art. 6 DSGVO", "DSGVO", "EU", 0.66), + } + out := rerankByAuthority("Welche Rechtsgrundlagen erlauben eine Verarbeitung personenbezogener Daten?", in) + if out[0].RegulationShort != "DSGVO" { + t.Fatalf("DSGVO Art.6 must beat general BDSG §, got %q", out[0].ArticleLabel) + } + if len(out) != 2 { + t.Fatalf("BDSG must stay visible (soft demote), got len=%d", len(out)) + } + }) + + t.Run("subsidiarity dp_08: BDSG §70 below DSGVO Art.28 (Auftragsverarbeitung)", func(t *testing.T) { + in := []LegalSearchResult{ + bindingRes("§ 70 BDSG", "BDSG", "DE", 0.70), // Teil 3 → scope + subsidiarity + bindingRes("Art. 28 DSGVO", "DSGVO", "EU", 0.66), + } + out := rerankByAuthority("Was muss ein Auftragsverarbeitungsvertrag enthalten?", in) + if out[0].RegulationShort != "DSGVO" { + t.Fatalf("DSGVO Art.28 must beat BDSG §70, got %q", out[0].ArticleLabel) + } + }) + + t.Run("subsidiarity dp_11: BDSG §22 below DSGVO Art.32 on a TOM question", func(t *testing.T) { + in := []LegalSearchResult{ + bindingRes("§ 22 BDSG", "BDSG", "DE", 0.70), + bindingRes("Art. 32 DSGVO", "DSGVO", "EU", 0.66), + } + out := rerankByAuthority("Welche technischen und organisatorischen Massnahmen verlangt das Datenschutzrecht?", in) + if out[0].RegulationShort != "DSGVO" { + t.Fatalf("DSGVO Art.32 must beat BDSG §22 on a non-topic TOM question, got %q", out[0].ArticleLabel) + } + }) + + t.Run("cr_07: a 'DSGVO' mention scopes the domain so BDSG Teil-3 §64 is demoted", func(t *testing.T) { + in := []LegalSearchResult{ + bindingRes("§ 64 BDSG", "BDSG", "DE", 0.70), // Teil 3 (law enforcement) + bindingRes("Art. 32 DSGVO", "DSGVO", "EU", 0.66), + } + // Query has no DP keyword but names the DSGVO → domain fallback scopes it data_protection, + // so scope+subsidiarity demote the law-enforcement § below the primary norm. + out := rerankByAuthority("Welche rechtliche Grundlage gilt fuer technische und organisatorische Massnahmen - DSGVO oder ein Standard?", in) + if out[0].RegulationShort != "DSGVO" { + t.Fatalf("DSGVO must win on a DSGVO-mention question, got %q", out[0].ArticleLabel) + } + }) + + t.Run("ePrivacy: a cookie query lifts §25 TDDDG above DSGVO consent (lex specialis topic)", func(t *testing.T) { + in := []LegalSearchResult{ + bindingRes("Art. 7 DSGVO", "DSGVO", "EU", 0.70), // higher semantic + bindingRes("§ 25 TDDDG", "TDDDG", "DE", 0.66), + } + out := rerankByAuthority("Wann ist eine Einwilligung fuer das Speichern von Cookies auf Endgeraeten erforderlich?", in) + if out[0].RegulationShort != "TDDDG" { + t.Fatalf("§25 TDDDG must win a cookie question (lex specialis topic), got %q", out[0].ArticleLabel) + } + }) + + t.Run("a general consent question still resolves to DSGVO, not §25 TDDDG", func(t *testing.T) { + in := []LegalSearchResult{ + bindingRes("§ 25 TDDDG", "TDDDG", "DE", 0.70), // higher semantic but no cookie topic + bindingRes("Art. 7 DSGVO", "DSGVO", "EU", 0.66), + } + out := rerankByAuthority("Welche Anforderungen gelten an eine wirksame Einwilligung?", in) + if out[0].RegulationShort != "DSGVO" { + t.Fatalf("a general consent question must resolve to DSGVO (TDDDG demoted), got %q", out[0].ArticleLabel) + } + }) + + t.Run("co-primary dp_01: BDSG §38 stays top on a DSB question (national special rule)", func(t *testing.T) { + in := []LegalSearchResult{ + bindingRes("§ 38 BDSG", "BDSG", "DE", 0.66), + bindingRes("Art. 37 DSGVO", "DSGVO", "EU", 0.64), + } + out := rerankByAuthority("Ab wann muss ein Datenschutzbeauftragter benannt werden?", in) + // DSB topic → §38 is co-primary (topic-matched, NOT subsidiarity-demoted) and keeps its + // semantic lead; Art. 37 stays a close second. Both remain top-2. + if out[0].RegulationShort != "BDSG" { + t.Fatalf("BDSG §38 (DSB co-primary) must stay top, got %q", out[0].ArticleLabel) + } + if out[1].RegulationShort != "DSGVO" { + t.Fatalf("Art. 37 DSGVO must stay co-primary second, got %q", out[1].ArticleLabel) + } + }) + t.Run("nothing is dropped and topic amplifies", func(t *testing.T) { in := []LegalSearchResult{ guidanceRes("ENISA", "ENISA", 0.72), diff --git a/ai-compliance-sdk/internal/ucca/authority_router.go b/ai-compliance-sdk/internal/ucca/authority_router.go new file mode 100644 index 00000000..98d006bf --- /dev/null +++ b/ai-compliance-sdk/internal/ucca/authority_router.go @@ -0,0 +1,129 @@ +package ucca + +import ( + "context" + "os" + "sort" + "strings" + "sync" +) + +// routerBaseCollections is the broad authority base the Authority Router fans out over. It mirrors +// the Advisor's historical multi-collection set; the KB-2026.1 slice is added separately when the +// query is in scope. Override via RAG_ROUTER_COLLECTIONS (comma-separated) per environment. +func (c *LegalRAGClient) routerBaseCollections() []string { + if v := strings.TrimSpace(os.Getenv("RAG_ROUTER_COLLECTIONS")); v != "" { + var out []string + for _, p := range strings.Split(v, ",") { + if s := strings.TrimSpace(p); s != "" { + out = append(out, s) + } + } + if len(out) > 0 { + return out + } + } + return []string{ + "bp_compliance_gesetze", + "bp_compliance_ce", + "bp_compliance_datenschutz", + "bp_dsfa_corpus", + "bp_compliance_recht", + "bp_legal_templates", + } +} + +const routerPerCollectionTopK = 3 + +// Retrieve is the Authority Router entry point: callers (the Advisor) pass ONLY a query and stay +// collection-agnostic. The router fans out over the broad authority base and ADDS the KB-2026.1 +// slice when the query is in scope (inKBScope), then merges all hits, deduplicates, and returns the +// top-K by authority score. This moves the former Advisor-side collection fan-out into the retrieval +// layer (the "Retriever" tier of the quality pyramid), so the proven KB-2026.1 slice gain reaches +// the product path without the Advisor knowing about individual collections. +// +// The merged set is ordered by the per-collection authority score that rerankByAuthority already +// produced inside searchInternal — i.e. binding-vs-guidance ordering is preserved across the merge. +// Per-collection failures (e.g. a collection absent on an environment) degrade gracefully. +func (c *LegalRAGClient) Retrieve(ctx context.Context, query string, topK int) ([]LegalSearchResult, error) { + if topK <= 0 { + topK = 8 + } + + collections := c.routerBaseCollections() + if c.kbScopeRoutingEnabled && c.kbSliceCollection != "" && inKBScope(query) { + collections = append(collections, c.kbSliceCollection) + } + + // Cross-regulation queries (>=2 explicitly named regulations) get a larger per-collection budget + // so each collection's multi-regulation search isn't truncated down to the keyword-dominant + // domain; the final per-regulation balancing then guarantees every named domain in the top-K. + regs := detectRegulations(query) + perColl := routerPerCollectionTopK + if len(regs) >= 2 { + perColl = routerPerCollectionTopK * len(regs) + } + + // Warm the full-text indexes sequentially first so the concurrent fan-out below only READS the + // shared textIndexEnsured map (the writes happen here, serialized) — closes the cold-start map + // race deterministically. Best-effort: a missing collection just stays un-indexed (hybrid then + // falls back to dense, or the per-collection search degrades to nothing). + if c.hybridEnabled { + for _, coll := range collections { + _ = c.ensureTextIndex(ctx, coll) + } + } + + out := make([][]LegalSearchResult, len(collections)) + var wg sync.WaitGroup + for i, coll := range collections { + wg.Add(1) + go func(i int, coll string) { + defer wg.Done() + if res, err := c.searchInternal(ctx, coll, query, nil, perColl); err == nil { + out[i] = res + } + }(i, coll) + } + wg.Wait() + + merged := make([]LegalSearchResult, 0, len(collections)*perColl) + for _, r := range out { + merged = append(merged, r...) + } + merged = dedupResults(merged) + sort.SliceStable(merged, func(a, b int) bool { return merged[a].Score > merged[b].Score }) + + // Cross-regulation: guarantee every named domain is represented (0070-class fix) instead of + // letting a global score-sort starve the non-dominant domain. + if len(regs) >= 2 { + return balanceByRegulation(merged, regs, topK), nil + } + if len(merged) > topK { + merged = merged[:topK] + } + return merged, nil +} + +// dedupResults removes duplicate passages that can appear when collections overlap, keeping the +// highest-scoring occurrence. Identity = regulation_code + article_label + a text prefix. +func dedupResults(in []LegalSearchResult) []LegalSearchResult { + pos := make(map[string]int, len(in)) + out := make([]LegalSearchResult, 0, len(in)) + for _, r := range in { + text := r.Text + if len(text) > 80 { + text = text[:80] + } + key := r.RegulationCode + "|" + r.ArticleLabel + "|" + text + if idx, ok := pos[key]; ok { + if r.Score > out[idx].Score { + out[idx] = r + } + continue + } + pos[key] = len(out) + out = append(out, r) + } + return out +} diff --git a/ai-compliance-sdk/internal/ucca/authority_router_e2e_test.go b/ai-compliance-sdk/internal/ucca/authority_router_e2e_test.go new file mode 100644 index 00000000..e8351cfc --- /dev/null +++ b/ai-compliance-sdk/internal/ucca/authority_router_e2e_test.go @@ -0,0 +1,164 @@ +package ucca + +import ( + "context" + "encoding/json" + "os" + "strconv" + "strings" + "testing" +) + +type benchQ struct { + ID string `json:"id"` + Document string `json:"document"` + Question string `json:"question"` +} + +// docTokens maps a bench question's expected document to acceptable regulation_code/label substrings. +func docTokens(document string) []string { + d := strings.ToUpper(document) + var t []string + for _, wp := range []string{"WP243", "WP248", "WP260"} { + if strings.Contains(d, wp) { + t = append(t, wp) + } + } + dns := strings.ReplaceAll(d, " ", "") + for _, gl := range []struct{ key, tok string }{{"07/2020", "GL07"}, {"05/2020", "GL05"}, {"09/2022", "GL09"}} { + if strings.Contains(dns, gl.key) { + t = append(t, gl.tok) + } + } + if strings.Contains(d, "TDDDG") { + t = append(t, "TDDDG") + } + if strings.Contains(d, "DSGVO") || strings.Contains(d, "ART. 13") || strings.Contains(d, "ART. 14") { + t = append(t, "DSGVO") + } + if strings.Contains(d, "BDSG") { + t = append(t, "BDSG") + } + if strings.Contains(d, "CRA") { + t = append(t, "CRA") + } + if strings.Contains(d, "MASCH") { + t = append(t, "MASCH", "MACHINERY", "MVO") + } + return t +} + +func hitDoc(results []LegalSearchResult, toks []string) bool { + for _, r := range results { + s := strings.ReplaceAll(strings.ToUpper(r.RegulationCode+" "+r.ArticleLabel), " ", "") + for _, tk := range toks { + if strings.Contains(s, strings.ReplaceAll(tk, " ", "")) { + return true + } + } + } + return false +} + +// TestMultiReg0070E2E (RUN_E2E=1) is the 0070 regression: a cross-regulation query (CRA + MaschVO) +// must return BOTH domains through the real Retrieve(), not just the keyword-dominant CRA. +func TestMultiReg0070E2E(t *testing.T) { + if os.Getenv("RUN_E2E") != "1" { + t.Skip("set RUN_E2E=1 + QDRANT_URL/OLLAMA_URL/QDRANT_API_KEY") + } + c := NewLegalRAGClient() + q := "Wie greifen CRA und Maschinenverordnung bei einer vernetzten Maschine ineinander?" + res, err := c.Retrieve(context.Background(), q, 8) + if err != nil { + t.Fatalf("retrieve: %v", err) + } + var hasCRA, hasMasch bool + var codes []string + for _, r := range res { + u := strings.ToUpper(r.RegulationCode) + codes = append(codes, u) + if strings.Contains(u, "CRA") { + hasCRA = true + } + if strings.Contains(u, "MASCH") || strings.Contains(u, "MACHIN") || u == "MVO" { + hasMasch = true + } + } + t.Logf("0070 top-8 codes: %v", codes) + if !hasCRA || !hasMasch { + t.Errorf("0070 must return BOTH domains via Retrieve(): CRA=%v MaschVO=%v", hasCRA, hasMasch) + } +} + +// TestAuthorityRouterCB100 (RUN_E2E=1) drives the REAL Retrieve() over the ComplianceBench-100 against +// the live collections: NEW (scope routing on → slice added for in-scope queries) vs OLD (routing off +// → broad base only). It is the regression gate that the router actually delivers the proven slice +// gain (+28/0-regr in the offline simulation) through the production Go code path. +func TestAuthorityRouterCB100(t *testing.T) { + if os.Getenv("RUN_E2E") != "1" { + t.Skip("set RUN_E2E=1 + QDRANT_URL/OLLAMA_URL/QDRANT_API_KEY + BENCH_PATH") + } + path := os.Getenv("BENCH_PATH") + if path == "" { + path = "/tmp/compliance_bench.json" + } + raw, err := os.ReadFile(path) + if err != nil { + t.Fatalf("bench read: %v", err) + } + var doc struct { + Questions []benchQ `json:"questions"` + } + if err := json.Unmarshal(raw, &doc); err != nil { + t.Fatalf("bench parse: %v", err) + } + + // BENCH_STRIDE samples every Kth question (stratified across DS/CRA/MaschVO) so the gate stays + // tractable against the remote dev Qdrant; default 1 = full CB-100. + stride := 1 + if s := os.Getenv("BENCH_STRIDE"); s != "" { + if n, err := strconv.Atoi(s); err == nil && n > 0 { + stride = n + } + } + + c := NewLegalRAGClient() + ctx := context.Background() + var n, oldHit, newHit, gain, regr int + for i, q := range doc.Questions { + if i%stride != 0 { + continue + } + n++ + toks := docTokens(q.Document) + c.kbScopeRoutingEnabled = false + oldRes, _ := c.Retrieve(ctx, q.Question, 8) + c.kbScopeRoutingEnabled = true + newRes, _ := c.Retrieve(ctx, q.Question, 8) + oh, nh := hitDoc(oldRes, toks), hitDoc(newRes, toks) + if oh { + oldHit++ + } + if nh { + newHit++ + } + flip := "=" + switch { + case !oh && nh: + gain++ + flip = "GAIN" + case oh && !nh: + regr++ + flip = "REGR" + } + t.Logf("%-9s [%-14s] OLD=%-5v NEW=%-5v %s", q.ID, q.Document, oh, nh, flip) + } + t.Logf("CB-100 sample (stride=%d) via Retrieve(): N=%d | OLD-hit %d | NEW-hit %d | GAIN %d | REGR %d", + stride, n, oldHit, newHit, gain, regr) + if newHit <= oldHit || gain < 3 { + t.Errorf("router must add slice gains: NEW(%d) must exceed OLD(%d), gain=%d", newHit, oldHit, gain) + } + if regr > 2 { + t.Errorf("too many regressions through the router: %d", regr) + } +} diff --git a/ai-compliance-sdk/internal/ucca/authority_router_test.go b/ai-compliance-sdk/internal/ucca/authority_router_test.go new file mode 100644 index 00000000..a08d7fd3 --- /dev/null +++ b/ai-compliance-sdk/internal/ucca/authority_router_test.go @@ -0,0 +1,99 @@ +package ucca + +import ( + "os" + "testing" +) + +func TestRouterBaseCollections(t *testing.T) { + c := &LegalRAGClient{} + os.Unsetenv("RAG_ROUTER_COLLECTIONS") + def := c.routerBaseCollections() + if len(def) != 6 || def[1] != "bp_compliance_ce" { + t.Fatalf("default base collections unexpected: %v", def) + } + + os.Setenv("RAG_ROUTER_COLLECTIONS", " bp_compliance_ce , kb_2026_1_build ,, ") + defer os.Unsetenv("RAG_ROUTER_COLLECTIONS") + got := c.routerBaseCollections() + if len(got) != 2 || got[0] != "bp_compliance_ce" || got[1] != "kb_2026_1_build" { + t.Fatalf("env override parse failed (trim/empty): %v", got) + } +} + +func TestRouterSliceSelection(t *testing.T) { + // The router appends the slice exactly when the query is in scope (inKBScope) and routing is on. + // Mirror the selection logic so a regression in either is caught without a live Qdrant. + c := &LegalRAGClient{kbSliceCollection: "kb_2026_1_build", kbScopeRoutingEnabled: true} + sel := func(q string) bool { + colls := c.routerBaseCollections() + if c.kbScopeRoutingEnabled && c.kbSliceCollection != "" && inKBScope(q) { + colls = append(colls, c.kbSliceCollection) + } + for _, x := range colls { + if x == c.kbSliceCollection { + return true + } + } + return false + } + if !sel("Welche neun Kriterien nennt WP248 fuer ein hohes Risiko?") { + t.Error("in-scope guidance query must include the slice") + } + if sel("Was sagt NIST SP 800-53 zu Access Control?") { + t.Error("out-of-scope query must NOT include the slice") + } + c.kbScopeRoutingEnabled = false + if sel("Welche Kriterien nennt WP248?") { + t.Error("routing disabled => slice never included") + } +} + +func TestBalanceByRegulation(t *testing.T) { + regs := []detectedRegulation{ + {Canonical: "CRA", CodeValues: []string{"CRA"}}, + {Canonical: "MaschVO", CodeValues: []string{"MASCHVO", "MVO", "MACHINERY"}}, + } + // CRA dominates by score; without balancing the top-4 would be all CRA + NIST. + pool := []LegalSearchResult{ + {RegulationCode: "CRA", Score: 0.99}, + {RegulationCode: "CRA", Score: 0.98}, + {RegulationCode: "CRA", Score: 0.97}, + {RegulationCode: "NIST", Score: 0.96}, + {RegulationCode: "MACHINERY", Score: 0.70}, + {RegulationCode: "MVO", Score: 0.65}, + } + out := balanceByRegulation(pool, regs, 4) + var hasCRA, hasMasch bool + for _, r := range out { + switch r.RegulationCode { + case "CRA": + hasCRA = true + case "MACHINERY", "MVO": + hasMasch = true + } + } + if !hasCRA || !hasMasch { + t.Errorf("both named domains must be represented: CRA=%v MaschVO=%v out=%v", hasCRA, hasMasch, out) + } + if out[0].RegulationCode != "CRA" || !(out[1].RegulationCode == "MACHINERY" || out[1].RegulationCode == "MVO") { + t.Errorf("round-robin should alternate domains, got %s then %s", out[0].RegulationCode, out[1].RegulationCode) + } +} + +func TestDedupResults(t *testing.T) { + in := []LegalSearchResult{ + {RegulationCode: "EDPB WP248", ArticleLabel: "III.B", Text: "lorem", Score: 0.7}, + {RegulationCode: "EDPB WP248", ArticleLabel: "III.B", Text: "lorem", Score: 0.9}, // dup, higher score + {RegulationCode: "DSGVO", ArticleLabel: "Art. 35", Text: "ipsum", Score: 0.8}, + } + out := dedupResults(in) + if len(out) != 2 { + t.Fatalf("expected 2 deduped, got %d", len(out)) + } + for _, r := range out { + if r.RegulationCode == "EDPB WP248" && r.Score != 0.9 { + t.Errorf("dedup must keep highest score, got %v", r.Score) + } + } +} diff --git a/ai-compliance-sdk/internal/ucca/guidance_fix_e2e_test.go b/ai-compliance-sdk/internal/ucca/guidance_fix_e2e_test.go new file mode 100644 index 00000000..d97c6e16 --- /dev/null +++ b/ai-compliance-sdk/internal/ucca/guidance_fix_e2e_test.go @@ -0,0 +1,105 @@ +package ucca + +import ( + "context" + "encoding/json" + "fmt" + "os" + "strings" + "testing" +) + +// TestGuidanceFixE2E runs the 10 hard cases through the REAL LegalRAGClient against the +// homogeneous build collection. Guarded by RUN_E2E=1. Reports the rank of the expected +// document within the returned top-K — proving whether the guidanceIntentSignals fix lifts +// guidance (WP248/WP260) back into the prompt. Toggle RAG_HYBRID_SEARCH to compare modes. +func TestGuidanceFixE2E(t *testing.T) { + if os.Getenv("RUN_E2E") != "1" { + t.Skip("set RUN_E2E=1 + QDRANT_URL/OLLAMA_URL to run") + } + c := NewLegalRAGClient() + coll := os.Getenv("E2E_COLLECTION") + if coll == "" { + coll = "bp_compliance_kb_2026_1_build" + } + cases := []struct{ id, q, expect string }{ + {"GQ-0012", "Welche neun Kriterien nennt WP248 fuer ein voraussichtlich hohes Risiko?", "WP248"}, + {"GQ-0013", "Ab wie vielen der WP248-Kriterien ist in der Regel eine Datenschutz-Folgenabschaetzung erforderlich?", "WP248"}, + {"GQ-0023", "Welche Anforderungen stellt WP260 an eine klare und einfache Sprache?", "WP260"}, + {"GQ-0024", "Was versteht WP260 unter Layered Privacy Notices?", "WP260"}, + {"GQ-0054", "Welche grundlegenden Cybersecurity-Anforderungen enthaelt Annex I Part I?", "CRA"}, + {"GQ-0060", "Wann muss eine aktiv ausgenutzte Schwachstelle gemeldet werden?", "CRA"}, + {"GQ-0074", "Benoetigt eine SPS ohne Netzwerkanschluss eine CRA-Bewertung?", "CRA"}, + {"GQ-0079", "Welche grundlegenden Sicherheits- und Gesundheitsschutzanforderungen enthaelt Anhang III?", "MASCHVO"}, + {"GQ-0091", "Welche Anforderungen gelten fuer wesentliche Veraenderungen einer Maschine?", "MASCHVO"}, + {"GQ-0070", "Wie greifen CRA und Maschinenverordnung bei einer vernetzten Maschine ineinander?", "CRA"}, + } + fmt.Printf("\n### hybrid=%v collection=%s\n", os.Getenv("RAG_HYBRID_SEARCH") != "false", coll) + for _, tc := range cases { + res, err := c.SearchCollection(context.Background(), coll, tc.q, nil, 8) + if err != nil { + t.Fatalf("%s: %v", tc.id, err) + } + rank := -1 + for i, r := range res { + lab := strings.ToUpper(r.RegulationCode + " " + r.ArticleLabel) + if strings.Contains(lab, tc.expect) { + rank = i + 1 + break + } + } + top1 := "" + if len(res) > 0 { + top1 = res[0].RegulationCode + " (" + res[0].SourceClass + ")" + } + status := "FAIL" + if rank > 0 { + status = "OK" + } + fmt.Printf("%-9s expect=%-8s rank_in_top8=%-2d %-5s top1=%s\n", tc.id, tc.expect, rank, status, top1) + } +} + +// TestBenchE2E runs the FULL ComplianceBench (E2E_BENCH_FILE) through the real client and +// prints, per question, the ordered top-8 regulation codes. Diffing BEFORE vs AFTER proves +// the fix only perturbs guidance-intent queries (gated on queryWantsGuidance) and never the +// norm questions — the Knowledge-Freeze regression guard. +func TestBenchE2E(t *testing.T) { + if os.Getenv("RUN_E2E") != "1" { + t.Skip("set RUN_E2E=1 + E2E_BENCH_FILE") + } + path := os.Getenv("E2E_BENCH_FILE") + if path == "" { + t.Skip("E2E_BENCH_FILE not set") + } + raw, err := os.ReadFile(path) + if err != nil { + t.Fatal(err) + } + var bench struct { + Questions []struct { + ID string `json:"id"` + Question string `json:"question"` + } `json:"questions"` + } + if err := json.Unmarshal(raw, &bench); err != nil { + t.Fatal(err) + } + c := NewLegalRAGClient() + coll := os.Getenv("E2E_COLLECTION") + if coll == "" { + coll = "bp_compliance_kb_2026_1_build" + } + fmt.Printf("### BENCH n=%d hybrid=%v\n", len(bench.Questions), os.Getenv("RAG_HYBRID_SEARCH") != "false") + for _, q := range bench.Questions { + res, err := c.SearchCollection(context.Background(), coll, q.Question, nil, 8) + if err != nil { + t.Fatalf("%s: %v", q.ID, err) + } + codes := make([]string, 0, len(res)) + for _, r := range res { + codes = append(codes, strings.ReplaceAll(r.RegulationCode, ";", ",")) + } + fmt.Printf("BENCH|%s|%s\n", q.ID, strings.Join(codes, ";")) + } +} diff --git a/ai-compliance-sdk/internal/ucca/kb_scope_routing.go b/ai-compliance-sdk/internal/ucca/kb_scope_routing.go new file mode 100644 index 00000000..5f765f81 --- /dev/null +++ b/ai-compliance-sdk/internal/ucca/kb_scope_routing.go @@ -0,0 +1,52 @@ +package ucca + +import "strings" + +// kbScopeTopics are high-precision data-protection / compliance topic markers that place a query in +// the KB-2026.1 authoritative slice even when it does NOT name a regulation. Conservative by design: +// an unmatched query falls back to the broad CE default (no regression) — the slice is only used when +// the query is confidently in-scope. +var kbScopeTopics = []string{ + // DP-Guidance-Marker, die IN der Slice liegen (EDPB/DSK/WP/GL) — bewusst NICHT die generischen + // Verben aus guidanceIntentSignals (sagt/laut/empfiehlt/auslegung) und NICHT enisa/bsi/nist/owasp + // (die liegen im breiten CE-Pool, nicht in der Slice). + "edpb", "dsk", "datenschutzausschuss", "orientierungshilfe", + "wp2", "wp 2", "wp29", "working paper", "gl 0", + "datenschutz", "dsgvo", "gdpr", "dsfa", "folgenabschätzung", "folgenabschaetzung", + "einwilligung", "auftragsverarbeit", "betroffenenrecht", "auskunftsrecht", + "verarbeitungsverzeichnis", "datenschutzbeauftragt", "verzeichnis von verarbeitung", + "cookie", "tracking", "transparenzpflicht", "datenpanne", "meldepflicht", + "technische und organisatorische maßnahmen", + "cyber resilience", "schwachstelle", "vulnerability", "sicherheitsupdate", + "maschinensicherheit", "wesentliche veränderung", "wesentliche veraenderung", + "konformitätsbewertung", "konformitaetsbewertung", "ce-kennzeichnung", +} + +// inKBScope reports whether the query belongs to the KB-2026.1 authoritative slice. True when it +// names an in-slice regulation (detectRegulations), asks for guidance (EDPB/DSK/WP/GL), or hits a +// data-protection / compliance topic marker. +func inKBScope(query string) bool { + if len(detectRegulations(query)) > 0 { + return true + } + q := strings.ToLower(query) + for _, t := range kbScopeTopics { + if strings.Contains(q, t) { + return true + } + } + return false +} + +// resolveCollection applies the Blue-Green „authoritative slice promotion" routing. An explicitly +// requested collection is honoured unchanged; the DEFAULT (empty) request is routed to the KB-2026.1 +// slice when the query is in-scope, else to the broad CE default. Disable via RAG_KB_SCOPE_ROUTING=false. +func (c *LegalRAGClient) resolveCollection(query, requested string) string { + if requested != "" { + return requested + } + if c.kbScopeRoutingEnabled && c.kbSliceCollection != "" && inKBScope(query) { + return c.kbSliceCollection + } + return c.collection +} diff --git a/ai-compliance-sdk/internal/ucca/kb_scope_routing_test.go b/ai-compliance-sdk/internal/ucca/kb_scope_routing_test.go new file mode 100644 index 00000000..2a2823f8 --- /dev/null +++ b/ai-compliance-sdk/internal/ucca/kb_scope_routing_test.go @@ -0,0 +1,101 @@ +package ucca + +import ( + "context" + "fmt" + "os" + "strings" + "testing" +) + +func TestInKBScope(t *testing.T) { + inScope := []string{ + "Welche neun Kriterien nennt WP248 fuer ein hohes Risiko?", + "Wie greifen CRA und Maschinenverordnung bei einer vernetzten Maschine ineinander?", + "Wann ist eine Datenschutz-Folgenabschaetzung erforderlich?", + "Welche Anforderungen stellt die DSGVO an die Einwilligung?", + "Brauche ich einen Datenschutzbeauftragten?", + "Wann muss eine aktiv ausgenutzte Schwachstelle gemeldet werden?", + } + outScope := []string{ + "Welche OWASP-Kontrollen gibt es fuer Authentifizierung?", + "Was sagt NIST SP 800-53 zu Access Control?", + "Wie funktioniert ISO 27001 Zertifizierung?", + "Welche IFRS-Standards gelten fuer Leasing?", + } + for _, q := range inScope { + if !inKBScope(q) { + t.Errorf("inKBScope(%q) = false, want true", q) + } + } + for _, q := range outScope { + if inKBScope(q) { + t.Errorf("inKBScope(%q) = true, want false", q) + } + } +} + +func TestResolveCollection(t *testing.T) { + c := &LegalRAGClient{collection: "bp_compliance_ce", kbSliceCollection: "kb_2026_1_build", kbScopeRoutingEnabled: true} + if got := c.resolveCollection("Welche Kriterien nennt WP248?", ""); got != "kb_2026_1_build" { + t.Errorf("in-scope default -> %s, want kb_2026_1_build", got) + } + if got := c.resolveCollection("Was sagt NIST SP 800-53?", ""); got != "bp_compliance_ce" { + t.Errorf("out-of-scope default -> %s, want bp_compliance_ce", got) + } + if got := c.resolveCollection("Welche Kriterien nennt WP248?", "explicit_coll"); got != "explicit_coll" { + t.Errorf("explicit request must be honoured -> %s", got) + } + c.kbScopeRoutingEnabled = false + if got := c.resolveCollection("Welche Kriterien nennt WP248?", ""); got != "bp_compliance_ce" { + t.Errorf("disabled routing -> %s, want bp_compliance_ce", got) + } +} + +// TestKBScopeRoutingE2E (RUN_E2E=1) verifies the routing against the REAL collections: a default +// Search() of an in-scope query must hit the KB-2026.1 slice (WP248/MaschVO live there but NOT in +// the broad CE pool = clean discriminator); an out-of-scope query stays on CE. +func TestKBScopeRoutingE2E(t *testing.T) { + if os.Getenv("RUN_E2E") != "1" { + t.Skip("set RUN_E2E=1 + QDRANT_URL/OLLAMA_URL/QDRANT_API_KEY") + } + c := NewLegalRAGClient() + cases := []struct { + q string + wantToken string // expected in top-8 when routed to the slice + wantInKB bool + }{ + {"Welche neun Kriterien nennt WP248 fuer ein voraussichtlich hohes Risiko?", "WP248", true}, + {"Welche grundlegenden Sicherheits- und Gesundheitsschutzanforderungen enthaelt Anhang III der Maschinenverordnung?", "MASCH", true}, + {"Wie greifen CRA und Maschinenverordnung bei einer vernetzten Maschine ineinander?", "MASCH", true}, + {"Was sagt NIST SP 800-53 zu Access Control?", "", false}, + } + for _, tc := range cases { + routed := c.resolveCollection(tc.q, "") + res, err := c.Search(context.Background(), tc.q, nil, 8) + if err != nil { + t.Fatalf("%q: %v", tc.q, err) + } + codes := map[string]bool{} + for _, r := range res { + codes[strings.ToUpper(r.RegulationCode)] = true + } + hit := false + if tc.wantToken != "" { + for cd := range codes { + if strings.Contains(cd, tc.wantToken) { + hit = true + break + } + } + } + col := make([]string, 0, len(codes)) + for cd := range codes { + col = append(col, cd) + } + fmt.Printf("inKB=%-5v routed=%-16s wantTok=%-6s found=%-5v | %v\n", tc.wantInKB, routed, tc.wantToken, hit, col) + if tc.wantInKB && tc.wantToken != "" && !hit { + t.Errorf("%q routed to %s but %s not in top-8 (slice not active?)", tc.q, routed, tc.wantToken) + } + } +} diff --git a/ai-compliance-sdk/internal/ucca/legal_rag_client.go b/ai-compliance-sdk/internal/ucca/legal_rag_client.go index d0bb408e..db4993b7 100644 --- a/ai-compliance-sdk/internal/ucca/legal_rag_client.go +++ b/ai-compliance-sdk/internal/ucca/legal_rag_client.go @@ -21,6 +21,12 @@ type LegalRAGClient struct { textIndexEnsured map[string]bool hybridEnabled bool graphEnabled bool + + // Blue-Green „authoritative slice promotion" (additiv, KEIN CE-Ersatz): faellt eine Query + // in den KB-2026.1-Scope (DP/CRA/MaschVO/NIS2/DataAct/DORA/AIAct + EDPB/DSK-Guidance), wird + // die hochwertige Slice-Collection abgefragt; sonst bleibt der breite Default (bp_compliance_ce). + kbSliceCollection string + kbScopeRoutingEnabled bool } // NewLegalRAGClient creates a new Legal RAG client using Ollama bge-m3 embeddings. @@ -45,15 +51,25 @@ func NewLegalRAGClient() *LegalRAGClient { // zur Begruendung/Vollstaendigkeit genutzt, nicht zur Pool-Expansion (Default). graphEnabled := os.Getenv("RAG_GRAPH_EXPANSION") == "true" + // KB-2026.1 authoritative slice (Blue-Green, additiv). Routing default AN; Rollback ohne + // Redeploy ueber RAG_KB_SCOPE_ROUTING=false (dann faellt alles auf den CE-Default zurueck). + kbSlice := os.Getenv("RAG_KB_SLICE_COLLECTION") + if kbSlice == "" { + kbSlice = "kb_2026_1_build" + } + kbScopeRouting := os.Getenv("RAG_KB_SCOPE_ROUTING") != "false" + return &LegalRAGClient{ - qdrantURL: qdrantURL, - qdrantAPIKey: qdrantAPIKey, - ollamaURL: ollamaURL, - embeddingModel: "bge-m3", - collection: "bp_compliance_ce", - textIndexEnsured: make(map[string]bool), - hybridEnabled: hybridEnabled, - graphEnabled: graphEnabled, + qdrantURL: qdrantURL, + qdrantAPIKey: qdrantAPIKey, + ollamaURL: ollamaURL, + embeddingModel: "bge-m3", + collection: "bp_compliance_ce", + textIndexEnsured: make(map[string]bool), + hybridEnabled: hybridEnabled, + graphEnabled: graphEnabled, + kbSliceCollection: kbSlice, + kbScopeRoutingEnabled: kbScopeRouting, httpClient: &http.Client{ Timeout: 60 * time.Second, }, @@ -63,21 +79,32 @@ func NewLegalRAGClient() *LegalRAGClient { // SearchCollection queries a specific Qdrant collection for relevant passages. // If collection is empty, it falls back to the default collection (bp_compliance_ce). func (c *LegalRAGClient) SearchCollection(ctx context.Context, collection string, query string, regulationIDs []string, topK int) ([]LegalSearchResult, error) { - if collection == "" { - collection = c.collection - } - return c.searchInternal(ctx, collection, query, regulationIDs, topK) + return c.searchInternal(ctx, c.resolveCollection(query, collection), query, regulationIDs, topK) } -// Search queries the compliance CE corpus for relevant passages. +// Search queries the compliance corpus for relevant passages. The target collection is resolved by +// the Blue-Green slice routing: the KB-2026.1 slice for in-scope queries, else the broad CE default. func (c *LegalRAGClient) Search(ctx context.Context, query string, regulationIDs []string, topK int) ([]LegalSearchResult, error) { - return c.searchInternal(ctx, c.collection, query, regulationIDs, topK) + return c.searchInternal(ctx, c.resolveCollection(query, ""), query, regulationIDs, topK) } // searchInternal performs the actual search against a given collection. // If hybrid search is enabled, it uses the Qdrant Query API with RRF fusion // (dense + full-text). Falls back to dense-only /points/search on failure. func (c *LegalRAGClient) searchInternal(ctx context.Context, collection string, query string, regulationIDs []string, topK int) ([]LegalSearchResult, error) { + // Multi-Regulation-Retrieval: nennt die Query EXPLIZIT >=2 Regelwerke (z.B. "CRA und + // Maschinenverordnung"), wird pro Regelwerk separat retrieved + gemergt, damit BEIDE + // Domaenen im Prompt landen statt nur der keyword-dominanten. Generisch (Query->Regelwerke, + // keine doc-spezifische Logik); nur wenn der Caller nicht ohnehin schon auf Regulierungen + // filtert. Best-effort: leeres/fehlerhaftes Multi-Ergebnis faellt auf die Standardsuche zurueck. + if len(regulationIDs) == 0 { + if regs := detectRegulations(query); len(regs) >= 2 { + if mr, mErr := c.searchMultiRegulation(ctx, collection, query, regs, topK); mErr == nil && len(mr) > 0 { + return mr, nil + } + } + } + embedding, err := c.generateEmbedding(ctx, query) if err != nil { return nil, fmt.Errorf("failed to generate embedding: %w", err) @@ -123,43 +150,7 @@ func (c *LegalRAGClient) searchInternal(ctx context.Context, collection string, hits = c.expandViaGraph(ctx, collection, hits) } - results := make([]LegalSearchResult, len(hits)) - for i, hit := range hits { - // Legal-Metadaten nach rag_reingest_spec.md §2: bevorzugt die normalisierten Felder - // (article_label/regulation_code/article/...); Fallback auf alte Feldnamen, solange der - // Korpus noch nicht re-ingestiert ist (regulation_id, section="§ 38"). - regCode := getString(hit.Payload, "regulation_code") - if regCode == "" { - regCode = getString(hit.Payload, "regulation_id") - } - article := getString(hit.Payload, "article") - if article == "" { - article = getString(hit.Payload, "section") - } - results[i] = LegalSearchResult{ - Text: getString(hit.Payload, "chunk_text"), - RegulationCode: regCode, - RegulationName: getString(hit.Payload, "regulation_name_de"), - RegulationShort: getString(hit.Payload, "regulation_short"), - Category: getString(hit.Payload, "category"), - ArticleLabel: getString(hit.Payload, "article_label"), - Article: article, - Paragraph: getString(hit.Payload, "paragraph"), - Sub: getString(hit.Payload, "sub"), - IsRecital: getBool(hit.Payload, "is_recital"), - CitationStyle: getString(hit.Payload, "citation_style"), - Pages: getIntSlice(hit.Payload, "pages"), - SourceURL: getString(hit.Payload, "source"), - Score: hit.Score, - AuthorityWeight: getInt(hit.Payload, "authority_weight"), - SourceClass: getString(hit.Payload, "source_class"), - Jurisdiction: getString(hit.Payload, "jurisdiction"), - CitationUnit: getString(hit.Payload, "citation_unit"), - ReferencesOut: getStringSlice(hit.Payload, "references_out"), - ReferencesIn: getStringSlice(hit.Payload, "references_in"), - Superseded: getString(hit.Payload, "status") == "superseded", - } - } + results := hitsToResults(hits) // Authority-aware Re-Ranking: bindendes Recht der passenden Jurisdiktion/Domaene nach // oben, Guidance/Fremdrecht/Off-Domain runter (nichts wird geloescht). Reihenfolge only, diff --git a/ai-compliance-sdk/internal/ucca/legal_rag_http.go b/ai-compliance-sdk/internal/ucca/legal_rag_http.go index c9805d0a..dd660ed6 100644 --- a/ai-compliance-sdk/internal/ucca/legal_rag_http.go +++ b/ai-compliance-sdk/internal/ucca/legal_rag_http.go @@ -122,12 +122,14 @@ func (c *LegalRAGClient) searchHybrid(ctx context.Context, collection string, em } if len(regulationIDs) > 0 { - conditions := make([]qdrantCondition, len(regulationIDs)) - for i, regID := range regulationIDs { - conditions[i] = qdrantCondition{ - Key: "regulation_id", - Match: qdrantMatch{Value: regID}, - } + // Match BOTH the legacy field (regulation_id) and the normalized field + // (regulation_code) so per-regulation filtering works on the re-ingested corpus too. + conditions := make([]qdrantCondition, 0, len(regulationIDs)*2) + for _, regID := range regulationIDs { + conditions = append(conditions, + qdrantCondition{Key: "regulation_id", Match: qdrantMatch{Value: regID}}, + qdrantCondition{Key: "regulation_code", Match: qdrantMatch{Value: regID}}, + ) } queryReq.Filter = &qdrantFilter{Should: conditions} } @@ -175,12 +177,14 @@ func (c *LegalRAGClient) searchDense(ctx context.Context, collection string, emb } if len(regulationIDs) > 0 { - conditions := make([]qdrantCondition, len(regulationIDs)) - for i, regID := range regulationIDs { - conditions[i] = qdrantCondition{ - Key: "regulation_id", - Match: qdrantMatch{Value: regID}, - } + // Match BOTH the legacy field (regulation_id) and the normalized field + // (regulation_code) so per-regulation filtering works on the re-ingested corpus too. + conditions := make([]qdrantCondition, 0, len(regulationIDs)*2) + for _, regID := range regulationIDs { + conditions = append(conditions, + qdrantCondition{Key: "regulation_id", Match: qdrantMatch{Value: regID}}, + qdrantCondition{Key: "regulation_code", Match: qdrantMatch{Value: regID}}, + ) } searchReq.Filter = &qdrantFilter{Should: conditions} } diff --git a/ai-compliance-sdk/internal/ucca/multi_regulation.go b/ai-compliance-sdk/internal/ucca/multi_regulation.go new file mode 100644 index 00000000..890a5e2c --- /dev/null +++ b/ai-compliance-sdk/internal/ucca/multi_regulation.go @@ -0,0 +1,201 @@ +package ucca + +import ( + "context" + "fmt" + "strings" +) + +// multiRegMinPerRegulation is the minimum number of hits fetched per named regulation, so +// each domain is fairly represented even when topK/len(regs) would be tiny. +const multiRegMinPerRegulation = 3 + +// regulationCatalog maps a regulation to (a) the aliases that signal it is EXPLICITLY named +// in a query and (b) the regulation_code/regulation_id values used to filter the corpus. +// Deterministic + generic: a query naming >=2 regulations triggers per-regulation retrieval +// so a cross-regulation question returns every named domain — NOT a doc-specific rule. +var regulationCatalog = []struct { + Canonical string + Aliases []string + CodeValues []string +}{ + {"CRA", []string{"cra", "cyber resilience"}, []string{"CRA"}}, + // MaschVO heisst je Collection anders: Slice MASCHVO · gesetze MVO · ce MACHINERY/MASCHINENVO. + // Alle Varianten als CodeValues, sonst findet der per-Reg-Filter MaschVO nur in der Slice (0070). + {"MaschVO", []string{"maschinenverordnung", "maschvo", "machinery regulation"}, []string{"MASCHVO", "MaschVO", "MVO", "MASCHINENVO", "MACHINERY"}}, + {"NIS2", []string{"nis2", "nis-2", "nis 2"}, []string{"NIS2"}}, + {"DORA", []string{"dora"}, []string{"DORA"}}, + {"Data Act", []string{"data act", "datengesetz"}, []string{"DATA ACT", "DataAct"}}, + {"AI Act", []string{"ai act", "ki-vo", "ki-verordnung", "ai-verordnung"}, []string{"AI ACT", "AIAct"}}, + {"DSGVO", []string{"dsgvo", "gdpr"}, []string{"DSGVO"}}, + {"TDDDG", []string{"tdddg"}, []string{"TDDDG"}}, + {"BDSG", []string{"bdsg"}, []string{"BDSG"}}, +} + +type detectedRegulation struct { + Canonical string + CodeValues []string +} + +// detectRegulations returns the DISTINCT regulations explicitly named in the query. >=2 of +// them is the trigger for multi-regulation retrieval. Pure + deterministic, no LLM. +func detectRegulations(query string) []detectedRegulation { + q := strings.ToLower(query) + var out []detectedRegulation + for _, r := range regulationCatalog { + for _, a := range r.Aliases { + if strings.Contains(q, a) { + out = append(out, detectedRegulation{Canonical: r.Canonical, CodeValues: r.CodeValues}) + break + } + } + } + return out +} + +func hitID(h qdrantSearchHit) string { return fmt.Sprintf("%v", h.ID) } + +// balanceByRegulation builds the final top-K so EVERY explicitly-named regulation with hits is +// represented, instead of letting the keyword-dominant domain (e.g. CRA) crowd out the other +// (e.g. MaschVO) in a cross-regulation query. The input pool must already be score-ordered; +// results are grouped by exact regulation_code match against each regulation's CodeValues, then +// taken round-robin across the named domains (highest-scored first within each), with any +// remaining slots filled by the leftover pool in score order. Generic; no doc-specific logic. +func balanceByRegulation(pool []LegalSearchResult, regs []detectedRegulation, topK int) []LegalSearchResult { + if topK <= 0 { + topK = 8 + } + byReg := make([][]LegalSearchResult, len(regs)) + matched := make([]bool, len(pool)) + for ri, r := range regs { + for pi := range pool { + if matched[pi] { + continue + } + code := strings.TrimSpace(pool[pi].RegulationCode) + for _, cv := range r.CodeValues { + if strings.EqualFold(code, cv) { + byReg[ri] = append(byReg[ri], pool[pi]) + matched[pi] = true + break + } + } + } + } + out := make([]LegalSearchResult, 0, topK) + idx := make([]int, len(regs)) + for len(out) < topK { + progressed := false + for ri := range regs { + if idx[ri] < len(byReg[ri]) { + out = append(out, byReg[ri][idx[ri]]) + idx[ri]++ + progressed = true + if len(out) >= topK { + break + } + } + } + if !progressed { + break + } + } + for pi := range pool { + if len(out) >= topK { + break + } + if !matched[pi] { + out = append(out, pool[pi]) + } + } + return out +} + +// searchMultiRegulation retrieves each explicitly-named regulation SEPARATELY (per-regulation +// filter) and merges, so a cross-regulation query ("Wie greifen CRA und MaschVO ineinander?") +// returns BOTH domains in the prompt instead of only the keyword-dominant one. Generic over any +// named pair (DSGVO+TDDDG, CRA+NIS2, DORA+NIS2, AI Act+DSGVO, ...). The merged pool is +// authority-reranked once. Pure pool-construction; topK contract preserved. +func (c *LegalRAGClient) searchMultiRegulation(ctx context.Context, collection, query string, regs []detectedRegulation, topK int) ([]LegalSearchResult, error) { + embedding, err := c.generateEmbedding(ctx, query) + if err != nil { + return nil, fmt.Errorf("failed to generate embedding: %w", err) + } + perReg := topK / len(regs) + if perReg < multiRegMinPerRegulation { + perReg = multiRegMinPerRegulation + } + var merged []qdrantSearchHit + seen := make(map[string]bool) + for _, r := range regs { + var hits []qdrantSearchHit + if c.hybridEnabled { + if h, hErr := c.searchHybrid(ctx, collection, embedding, r.CodeValues, perReg); hErr == nil { + hits = h + } + } + if hits == nil { + if h, dErr := c.searchDense(ctx, collection, embedding, r.CodeValues, perReg); dErr == nil { + hits = h + } + } + for _, h := range hits { + id := hitID(h) + if seen[id] { + continue + } + seen[id] = true + merged = append(merged, h) + } + } + if len(merged) == 0 { + return nil, fmt.Errorf("multi-regulation search returned no hits") + } + results := hitsToResults(merged) + results = rerankByAuthority(query, results) + if topK > 0 && len(results) > topK { + results = results[:topK] + } + return results, nil +} + +// hitsToResults maps raw Qdrant hits to LegalSearchResult, preferring the normalized payload +// fields (regulation_code/article_label/...) with fallback to the legacy names (regulation_id, +// section) while the corpus is mid-re-ingestion. Shared by searchInternal + searchMultiRegulation. +func hitsToResults(hits []qdrantSearchHit) []LegalSearchResult { + results := make([]LegalSearchResult, len(hits)) + for i, hit := range hits { + regCode := getString(hit.Payload, "regulation_code") + if regCode == "" { + regCode = getString(hit.Payload, "regulation_id") + } + article := getString(hit.Payload, "article") + if article == "" { + article = getString(hit.Payload, "section") + } + results[i] = LegalSearchResult{ + Text: getString(hit.Payload, "chunk_text"), + RegulationCode: regCode, + RegulationName: getString(hit.Payload, "regulation_name_de"), + RegulationShort: getString(hit.Payload, "regulation_short"), + Category: getString(hit.Payload, "category"), + ArticleLabel: getString(hit.Payload, "article_label"), + Article: article, + Paragraph: getString(hit.Payload, "paragraph"), + Sub: getString(hit.Payload, "sub"), + IsRecital: getBool(hit.Payload, "is_recital"), + CitationStyle: getString(hit.Payload, "citation_style"), + Pages: getIntSlice(hit.Payload, "pages"), + SourceURL: getString(hit.Payload, "source"), + Score: hit.Score, + AuthorityWeight: getInt(hit.Payload, "authority_weight"), + SourceClass: getString(hit.Payload, "source_class"), + Jurisdiction: getString(hit.Payload, "jurisdiction"), + CitationUnit: getString(hit.Payload, "citation_unit"), + ReferencesOut: getStringSlice(hit.Payload, "references_out"), + ReferencesIn: getStringSlice(hit.Payload, "references_in"), + Superseded: getString(hit.Payload, "status") == "superseded", + } + } + return results +} diff --git a/ai-compliance-sdk/internal/ucca/multi_regulation_test.go b/ai-compliance-sdk/internal/ucca/multi_regulation_test.go new file mode 100644 index 00000000..69d7a423 --- /dev/null +++ b/ai-compliance-sdk/internal/ucca/multi_regulation_test.go @@ -0,0 +1,92 @@ +package ucca + +import ( + "context" + "fmt" + "os" + "strings" + "testing" +) + +// TestDetectRegulations is a pure unit test of the multi-regulation TRIGGER (no Qdrant): +// only an explicit naming of >=2 regulations enables multi-regulation retrieval. A single +// named regulation, or a topical question that doesn't name one, stays single-domain. +func TestDetectRegulations(t *testing.T) { + cases := []struct { + q string + want int + }{ + {"Welche neun Kriterien nennt WP248 fuer ein voraussichtlich hohes Risiko?", 0}, + {"Welche Anforderungen gelten fuer wesentliche Veraenderungen einer Maschine?", 0}, // "Maschine" != MaschVO + {"Benoetigt eine SPS ohne Netzwerkanschluss eine CRA-Bewertung?", 1}, // 1 -> single + {"Wie greifen CRA und Maschinenverordnung bei einer vernetzten Maschine ineinander?", 2}, + {"Wie greifen DSGVO und TDDDG bei der Nutzung von Cookies ineinander?", 2}, + {"Wie verhalten sich DORA und NIS2 fuer ein Finanzunternehmen?", 2}, + {"Wie greifen AI Act und DSGVO bei einem KI-System ineinander?", 2}, + } + for _, c := range cases { + if got := len(detectRegulations(c.q)); got != c.want { + t.Errorf("detectRegulations(%q) = %d, want %d", c.q, got, c.want) + } + } +} + +// TestMultiRegE2E (RUN_E2E=1) verifies against the build collection that an explicit +// cross-regulation query returns BOTH named domains in the top-K — the core acceptance +// gate for multi-regulation retrieval. +func TestMultiRegE2E(t *testing.T) { + if os.Getenv("RUN_E2E") != "1" { + t.Skip("set RUN_E2E=1 + QDRANT_URL/OLLAMA_URL") + } + c := NewLegalRAGClient() + coll := os.Getenv("E2E_COLLECTION") + if coll == "" { + coll = "bp_compliance_kb_2026_1_build" + } + cases := []struct { + id string + q string + want []string + }{ + {"GQ-0070 CRA+MaschVO", "Wie greifen CRA und Maschinenverordnung bei einer vernetzten Maschine ineinander?", []string{"CRA", "MASCH"}}, + {"DSGVO+TDDDG", "Wie greifen DSGVO und TDDDG bei der Nutzung von Cookies und Tracking-Technologien ineinander?", []string{"DSGVO", "TDDDG"}}, + {"CRA+NIS2", "Wie verhalten sich CRA und NIS2 bei einem vernetzten Produkt eines wichtigen Unternehmens zueinander?", []string{"CRA", "NIS2"}}, + {"DORA+NIS2", "Wie greifen DORA und NIS2 bei einem Finanzunternehmen ineinander?", []string{"DORA", "NIS2"}}, + {"AI Act+DSGVO", "Wie greifen AI Act und DSGVO bei einem KI-System ineinander, das personenbezogene Daten verarbeitet?", []string{"AI ACT", "DSGVO"}}, + } + for _, tc := range cases { + res, err := c.SearchCollection(context.Background(), coll, tc.q, nil, 8) + if err != nil { + t.Fatalf("%s: %v", tc.id, err) + } + present := map[string]bool{} + for _, r := range res { + present[strings.ToUpper(r.RegulationCode)] = true + } + ok := true + for _, w := range tc.want { + found := false + for cd := range present { + if strings.Contains(cd, w) { + found = true + break + } + } + if !found { + ok = false + } + } + codes := make([]string, 0, len(present)) + for cd := range present { + codes = append(codes, cd) + } + status := "OK" + if !ok { + status = "FAIL" + } + fmt.Printf("%-22s want=%v present=%v %s\n", tc.id, tc.want, codes, status) + if !ok { + t.Errorf("%s: not all named regulations in top-8 (want %v, got %v)", tc.id, tc.want, codes) + } + } +} diff --git a/backend-compliance/compliance/api/__init__.py b/backend-compliance/compliance/api/__init__.py index 8faf5866..3c47c08d 100644 --- a/backend-compliance/compliance/api/__init__.py +++ b/backend-compliance/compliance/api/__init__.py @@ -78,6 +78,7 @@ _ROUTER_MODULES = [ "template_rule_routes", "specialist_agent_routes", "reasoning_routes", + "onboarding_routes", ] _loaded_count = 0 diff --git a/backend-compliance/compliance/api/ai_routes.py b/backend-compliance/compliance/api/ai_routes.py index 6e2997f9..5e31818b 100644 --- a/backend-compliance/compliance/api/ai_routes.py +++ b/backend-compliance/compliance/api/ai_routes.py @@ -162,7 +162,7 @@ async def update_ai_system( db: Session = Depends(get_db), ): """Update an AI system.""" - from datetime import datetime + from datetime import datetime, timezone system = db.query(AISystemDB).filter(AISystemDB.id == system_id).first() if not system: @@ -226,7 +226,7 @@ async def assess_ai_system( db: Session = Depends(get_db), ): """Run AI Act risk assessment for an AI system.""" - from datetime import datetime + from datetime import datetime, timezone system = db.query(AISystemDB).filter(AISystemDB.id == system_id).first() if not system: diff --git a/backend-compliance/compliance/api/canonical_control_routes.py b/backend-compliance/compliance/api/canonical_control_routes.py index a8f80cdd..cac566fb 100644 --- a/backend-compliance/compliance/api/canonical_control_routes.py +++ b/backend-compliance/compliance/api/canonical_control_routes.py @@ -47,6 +47,8 @@ from compliance.services.canonical_control_service import ( _control_row, # re-exported for legacy test imports ) +logger = logging.getLogger(__name__) + router = APIRouter(prefix="/v1/canonical", tags=["canonical-controls"]) diff --git a/backend-compliance/compliance/api/dashboard_routes.py b/backend-compliance/compliance/api/dashboard_routes.py index a4d2fd79..88034085 100644 --- a/backend-compliance/compliance/api/dashboard_routes.py +++ b/backend-compliance/compliance/api/dashboard_routes.py @@ -14,7 +14,7 @@ Endpoints: """ import logging -from datetime import datetime, date, timedelta +from datetime import datetime, date, timedelta, timezone from calendar import month_abbr from typing import Optional, Dict, Any, List from decimal import Decimal diff --git a/backend-compliance/compliance/api/dsfa_routes.py b/backend-compliance/compliance/api/dsfa_routes.py index 024737fe..1bc0fe0c 100644 --- a/backend-compliance/compliance/api/dsfa_routes.py +++ b/backend-compliance/compliance/api/dsfa_routes.py @@ -26,10 +26,11 @@ versions). Module-level helpers re-exported for legacy tests. import logging from typing import Any, List, Optional -from fastapi import APIRouter, Depends, Query +from fastapi import APIRouter, Depends, HTTPException, Query from pydantic import BaseModel from fastapi.responses import Response from sqlalchemy.orm import Session +from sqlalchemy import text from classroom_engine.database import get_db from compliance.api._http_errors import translate_domain_errors @@ -484,6 +485,7 @@ async def list_dsfas( async def create_dsfa( request: DSFACreate, tenant_id: Optional[str] = Query(None), + db: Session = Depends(get_db), service: DSFAService = Depends(get_dsfa_service), ) -> dict[str, Any]: """Neue DSFA erstellen.""" diff --git a/backend-compliance/compliance/api/evidence_routes.py b/backend-compliance/compliance/api/evidence_routes.py index 29acba1b..1b7f201e 100644 --- a/backend-compliance/compliance/api/evidence_routes.py +++ b/backend-compliance/compliance/api/evidence_routes.py @@ -16,6 +16,11 @@ from the legacy path. """ import logging +import os +import json +import hashlib +import uuid as uuid_module +from datetime import datetime, timedelta from typing import Any, Optional from fastapi import APIRouter, Depends, File, HTTPException, Query, UploadFile @@ -30,14 +35,15 @@ from ..db import ( EvidenceConfidenceEnum, EvidenceTruthStatusEnum, ) -from ..db.models import EvidenceDB, ControlDB, AuditTrailDB +from ..db.models import EvidenceDB, AuditTrailDB from ..services.auto_risk_updater import AutoRiskUpdater -from ..services.evidence_service import EvidenceService +from ..services.evidence_service import EvidenceService, _update_risks as _update_risks_impl from .schemas import ( EvidenceCreate, EvidenceResponse, EvidenceListResponse, EvidenceRejectRequest, ) from .audit_trail_utils import log_audit_trail +from ._http_errors import translate_domain_errors logger = logging.getLogger(__name__) router = APIRouter(tags=["compliance-evidence"]) @@ -146,6 +152,7 @@ async def list_evidence( status: Optional[str] = None, page: Optional[int] = Query(None, ge=1, description="Page number (1-based)"), limit: Optional[int] = Query(None, ge=1, le=500, description="Items per page"), + db: Session = Depends(get_db), service: EvidenceService = Depends(get_evidence_service), ) -> EvidenceListResponse: """List evidence with optional filters and pagination.""" @@ -186,9 +193,11 @@ async def list_evidence( @router.post("/evidence", response_model=EvidenceResponse) async def create_evidence( evidence_data: EvidenceCreate, + db: Session = Depends(get_db), service: EvidenceService = Depends(get_evidence_service), ) -> EvidenceResponse: """Create new evidence record.""" + dsms_cid = None repo = EvidenceRepository(db) # Get control UUID @@ -257,6 +266,7 @@ async def create_evidence( @router.delete("/evidence/{evidence_id}") async def delete_evidence( evidence_id: str, + db: Session = Depends(get_db), service: EvidenceService = Depends(get_evidence_service), ) -> dict[str, Any]: """Delete an evidence record.""" @@ -275,6 +285,7 @@ async def upload_evidence( title: str = Query(...), file: UploadFile = File(...), description: Optional[str] = Query(None), + db: Session = Depends(get_db), service: EvidenceService = Depends(get_evidence_service), ) -> EvidenceResponse: """Upload evidence file.""" @@ -674,6 +685,7 @@ async def collect_ci_evidence( async def get_ci_evidence_status( control_id: Optional[str] = Query(None, description="Filter by control ID"), days: int = Query(30, description="Look back N days"), + db: Session = Depends(get_db), service: EvidenceService = Depends(get_evidence_service), ) -> dict[str, Any]: """Get CI/CD evidence collection status overview.""" @@ -681,70 +693,8 @@ async def get_ci_evidence_status( return service.ci_status(control_id, days) -# ---------------------------------------------------------------------------- -# Legacy re-exports for tests that import helpers directly. -# ---------------------------------------------------------------------------- - - if control_id: - ctrl_repo = ControlRepository(db) - control = ctrl_repo.get_by_control_id(control_id) - if control: - query = query.filter(EvidenceDB.control_id == control.id) - - evidence_list = query.order_by(EvidenceDB.collected_at.desc()).limit(100).all() - - # Group by control and calculate stats - control_stats = defaultdict(lambda: { - "total": 0, - "valid": 0, - "failed": 0, - "last_collected": None, - "evidence": [], - }) - - for e in evidence_list: - # Get control_id string - control = db.query(ControlDB).filter(ControlDB.id == e.control_id).first() - ctrl_id = control.control_id if control else "unknown" - - stats = control_stats[ctrl_id] - stats["total"] += 1 - if e.status: - if e.status.value == "valid": - stats["valid"] += 1 - elif e.status.value == "failed": - stats["failed"] += 1 - if not stats["last_collected"] or e.collected_at > stats["last_collected"]: - stats["last_collected"] = e.collected_at - - # Add evidence summary - stats["evidence"].append({ - "id": e.id, - "type": e.evidence_type, - "status": e.status.value if e.status else None, - "collected_at": e.collected_at.isoformat() if e.collected_at else None, - "ci_job_id": e.ci_job_id, - }) - - # Convert to list and sort - result = [] - for ctrl_id, stats in control_stats.items(): - result.append({ - "control_id": ctrl_id, - "total_evidence": stats["total"], - "valid_count": stats["valid"], - "failed_count": stats["failed"], - "last_collected": stats["last_collected"].isoformat() if stats["last_collected"] else None, - "recent_evidence": stats["evidence"][:5], - }) - - result.sort(key=lambda x: x["last_collected"] or "", reverse=True) - - return { - "period_days": days, - "total_evidence": len(evidence_list), - "controls": result, - } +# (Alte CI-Status-Implementierung entfernt — unerreichbarer Code nach `return +# service.ci_status(...)`; durch den Service ersetzt, `query` war nie initialisiert.) # ============================================================================ @@ -772,6 +722,7 @@ async def review_evidence( approval_status='first_approved'. A second (different) reviewer then sets second_reviewer and approval_status='approved'. """ + dsms_cid = None evidence = db.query(EvidenceDB).filter(EvidenceDB.id == evidence_id).first() if not evidence: raise HTTPException(status_code=404, detail=f"Evidence {evidence_id} not found") @@ -851,6 +802,7 @@ async def reject_evidence( db: Session = Depends(get_db), ): """Reject evidence (sets approval_status='rejected').""" + dsms_cid = None evidence = db.query(EvidenceDB).filter(EvidenceDB.id == evidence_id).first() if not evidence: raise HTTPException(status_code=404, detail=f"Evidence {evidence_id} not found") diff --git a/backend-compliance/compliance/api/onboarding_routes.py b/backend-compliance/compliance/api/onboarding_routes.py new file mode 100644 index 00000000..e13289c3 --- /dev/null +++ b/backend-compliance/compliance/api/onboarding_routes.py @@ -0,0 +1,82 @@ +"""Onboarding Advisor endpoint — exposes the existing Smart Onboarding Advisor at runtime. + +This adds NO new reasoning logic. It exposes the already-built, tested orchestration (Signal Producers +-> Normalizer -> Silent Knowledge Pass -> Advisor) through one runtime endpoint. No DB, no persistence. + + POST /onboarding/advisor-start — (company + certs + target + scanner findings) -> advisory payload + GET /onboarding/targets — the supported target ids +""" + +import logging +from typing import Dict, List, Optional + +from fastapi import APIRouter, HTTPException +from pydantic import BaseModel, Field + +from compliance.onboarding import ( + AdvisorMeasure, + AdvisorQuestion, + InferredAssumption, + ProducedSignal, + RejectedAssumption, +) +from compliance.services.onboarding_service import labels_for, run_advisor, supported_targets + +logger = logging.getLogger(__name__) +router = APIRouter(prefix="/onboarding", tags=["onboarding"]) + + +class OnboardingAdvisorRequest(BaseModel): + company: str = "" + industry: Optional[str] = None + products: List[str] = Field(default_factory=list) + markets: List[str] = Field(default_factory=list) + certifications: List[str] = Field(default_factory=list) + known_evidence: List[str] = Field(default_factory=list) + target: str = "CRA" + scanner_findings: List[ProducedSignal] = Field(default_factory=list) # adapters upstream produced these + + +class AdvisorResponse(BaseModel): + silent_intake_summary: str = "" + headline: str = "" + auto_detected: List[str] = Field(default_factory=list) + indications: List[str] = Field(default_factory=list) # partial signal: raises strength, still asked + inferred_assumptions: List[InferredAssumption] = Field(default_factory=list) + rejected_assumptions: List[RejectedAssumption] = Field(default_factory=list) + top_5_questions: List[AdvisorQuestion] = Field(default_factory=list) + capability_delta: List[str] = Field(default_factory=list) + top_measures: List[AdvisorMeasure] = Field(default_factory=list) + evidence_requests: List[str] = Field(default_factory=list) + unsupported_domains: List[str] = Field(default_factory=list) + completeness_summary: str = "" + capability_labels: Dict[str, str] = Field(default_factory=dict) # capability_id -> human label (DE) + + +@router.get("/targets") +def list_targets() -> dict: + return {"targets": supported_targets()} + + +@router.post("/advisor-start", response_model=AdvisorResponse) +def advisor_start_endpoint(req: OnboardingAdvisorRequest) -> AdvisorResponse: + if req.target not in supported_targets(): + raise HTTPException(status_code=404, detail="unsupported target '%s'; supported: %s" % (req.target, supported_targets())) + result, si_summary = run_advisor( + company=req.company, certifications=req.certifications, target=req.target, + signals=req.scanner_findings, known_evidence=req.known_evidence, + products=req.products, markets=req.markets, industry=req.industry or "") + surfaced = [ + *result.auto_detected, *result.indications, *result.capability_delta, + *(q.capability_id for q in result.next_best_questions), + *(c for a in result.inferred_assumptions for c in a.capabilities), + *(m.capability_id for m in result.top_measures), + ] + return AdvisorResponse( + silent_intake_summary=si_summary, headline=result.headline, auto_detected=result.auto_detected, + indications=result.indications, + inferred_assumptions=result.inferred_assumptions, rejected_assumptions=result.rejected_assumptions, + top_5_questions=result.next_best_questions, capability_delta=result.capability_delta, + top_measures=result.top_measures, evidence_requests=result.evidence_requests, + unsupported_domains=result.unsupported_domains, completeness_summary=result.completeness_summary, + capability_labels=labels_for(surfaced)) diff --git a/backend-compliance/compliance/api/routes.py b/backend-compliance/compliance/api/routes.py index a3693475..c0a20760 100644 --- a/backend-compliance/compliance/api/routes.py +++ b/backend-compliance/compliance/api/routes.py @@ -24,6 +24,7 @@ from fastapi.responses import FileResponse from sqlalchemy.orm import Session from classroom_engine.database import get_db +from ..db.models import EvidenceDB from .audit_trail_utils import log_audit_trail from ..db import ( @@ -310,6 +311,7 @@ async def list_controls_paginated( ) async def get_control( control_id: str, + db: Session = Depends(get_db), svc: ControlExportService = Depends(get_ctrl_export_service), ) -> ControlResponse: """Get a specific control by control_id.""" @@ -354,6 +356,7 @@ async def get_control( async def update_control( control_id: str, update: ControlUpdate, + db: Session = Depends(get_db), svc: ControlExportService = Depends(get_ctrl_export_service), ) -> ControlResponse: """Update a control.""" @@ -443,6 +446,7 @@ async def update_control( async def review_control( control_id: str, review: ControlReviewRequest, + db: Session = Depends(get_db), svc: ControlExportService = Depends(get_ctrl_export_service), ) -> ControlResponse: """Mark a control as reviewed with new status.""" diff --git a/backend-compliance/compliance/api/vvt_routes.py b/backend-compliance/compliance/api/vvt_routes.py index 66e44f6e..0cbb8d83 100644 --- a/backend-compliance/compliance/api/vvt_routes.py +++ b/backend-compliance/compliance/api/vvt_routes.py @@ -21,7 +21,7 @@ Phase 1 Step 4 refactor: handlers delegate to VVTService. import logging from typing import Any, List, Optional -from fastapi import APIRouter, Depends, Query, Request +from fastapi import APIRouter, Depends, HTTPException, Query, Request from fastapi.responses import StreamingResponse from sqlalchemy.orm import Session diff --git a/backend-compliance/compliance/completeness/__init__.py b/backend-compliance/compliance/completeness/__init__.py new file mode 100644 index 00000000..9ceab454 --- /dev/null +++ b/backend-compliance/compliance/completeness/__init__.py @@ -0,0 +1,24 @@ +"""Regulatory Completeness — auditable knowledge coverage, not confidence. + +An internal quality machine: for an assessment it reports identified vs assessed regulations and +justifies every open or excluded domain (corpus gap -> future_corpus; applicability uncertain -> +query_required). The metric is counts, never a single percentage. The product never claims full +coverage — it makes its own knowledge state transparent and auditable. Deterministic, no LLM, no +new corpus/meta-model class (freeze v1.0). +""" + +from __future__ import annotations + +from .engine import assess_completeness +from .schemas import ( + Assumption, CompletenessReport, CorpusStatus, DomainCoverage, Exclusion, +) + +__all__ = [ + "assess_completeness", + "CompletenessReport", + "CorpusStatus", + "DomainCoverage", + "Exclusion", + "Assumption", +] diff --git a/backend-compliance/compliance/completeness/engine.py b/backend-compliance/compliance/completeness/engine.py new file mode 100644 index 00000000..fcfdba60 --- /dev/null +++ b/backend-compliance/compliance/completeness/engine.py @@ -0,0 +1,89 @@ +"""Regulatory Completeness Engine — measure auditable knowledge coverage for an assessment. + +Separates what we IDENTIFIED (triggered regulations) from what we ASSESSED (validated corpus AND +determined applicability), and justifies every gap. Two kinds of „open": + - corpus gap — no validated corpus yet (e.g. Environmental) -> future_corpus + - applicability open — corpus exists but applicability is uncertain (Data Act) -> query_required +The metric is COUNTS, never a single percentage. The audit statement says plainly „wir bewerteten M +von N Domänen; K sind nicht im validierten Korpus und wurden bewusst nicht bewertet". + +Deterministic, computed-not-stored, no LLM, no new corpus/meta-model class (freeze v1.0). Python 3.9. +""" + +from __future__ import annotations + +from typing import Any, Dict, List, Optional + +from .schemas import ( + Assumption, CompletenessReport, CorpusStatus, DomainCoverage, Exclusion, +) + +_VALID = {s.value for s in CorpusStatus} + + +def _status(corpus_status: Dict[str, str], reg: str) -> CorpusStatus: + raw = corpus_status.get(reg, "unknown") + return CorpusStatus(raw) if raw in _VALID else CorpusStatus.UNKNOWN + + +def assess_completeness( + identified_regulations: List[str], + corpus_status: Dict[str, str], + uncertain: Optional[List[Dict[str, Any]]] = None, + assumptions: Optional[List[Dict[str, Any]]] = None, + assessed_obligations: int = 0, +) -> CompletenessReport: + """Build the auditable coverage report. + + `identified_regulations`: triggered/identified for this product. `corpus_status`: regulation -> + one of validated/draft/unsupported/unknown (curated/injected corpus registry). `uncertain`: + applicability-uncertain regulations [{regulation, deciding_question, reason}]. `assumptions`: + [{key, value, note}]. `assessed_obligations`: count from Execution (injected, default 0). + """ + ids = sorted(set(identified_regulations)) + unc = uncertain or [] + unc_subjects = {str(u.get("regulation") or u.get("subject")) for u in unc if (u.get("regulation") or u.get("subject"))} + + coverage = [DomainCoverage(regulation=r, status=_status(corpus_status, r)) for r in ids] + assessed = [r for r in ids if _status(corpus_status, r) == CorpusStatus.VALIDATED and r not in unc_subjects] + open_regs = [r for r in ids if r not in assessed] + open_corpora = [r for r in ids if _status(corpus_status, r) in (CorpusStatus.UNSUPPORTED, CorpusStatus.UNKNOWN)] + + exclusions: List[Exclusion] = [] + for u in unc: + subj = str(u.get("regulation") or u.get("subject") or "") + if not subj: + continue + exclusions.append(Exclusion( + subject=subj, reason=str(u.get("reason", "Anwendbarkeit unsicher")), + deciding_question=str(u.get("deciding_question", "")), resolution="query_required")) + for r in open_regs: + if r in unc_subjects: + continue + st = _status(corpus_status, r) + if st == CorpusStatus.DRAFT: + exclusions.append(Exclusion(subject=r, reason="Korpus in Bearbeitung (draft)", resolution="in_review")) + else: + exclusions.append(Exclusion(subject=r, reason="nicht im validierten Korpus", resolution="future_corpus")) + + covered_subjects = {e.subject for e in exclusions} + justification = (not open_regs) or set(open_regs) <= covered_subjects + assumptions_m = [Assumption(key=str(a.get("key", "")), value=str(a.get("value", "")), note=str(a.get("note", ""))) for a in (assumptions or [])] + + summary = "Identifiziert %d · bewertet %d · offen %d · Unsicherheiten %d · Begründung %s" % ( + len(ids), len(assessed), len(open_regs), len(unc), "ja" if justification else "nein") + if open_regs: + audit = ( + "Für dieses Produkt konnten wir %d von %d identifizierten regulatorischen Domänen vollständig " + "bewerten. %d weitere %s noch nicht Bestandteil des validierten Korpus bzw. anwendungsunsicher " + "und wurden deshalb bewusst nicht bewertet." % ( + len(assessed), len(ids), len(open_regs), "ist" if len(open_regs) == 1 else "sind")) + else: + audit = "Für dieses Produkt konnten wir alle %d identifizierten regulatorischen Domänen vollständig bewerten." % len(ids) + + return CompletenessReport( + identified_regulations=ids, assessed_regulations=assessed, open_regulations=open_regs, + open_corpora=open_corpora, coverage=coverage, assumptions=assumptions_m, exclusions=exclusions, + uncertainties_count=len(unc), assessed_obligations=assessed_obligations, + justification_present=justification, completeness_summary=summary, audit_statement=audit, + ) diff --git a/backend-compliance/compliance/completeness/schemas.py b/backend-compliance/compliance/completeness/schemas.py new file mode 100644 index 00000000..d9f7e694 --- /dev/null +++ b/backend-compliance/compliance/completeness/schemas.py @@ -0,0 +1,62 @@ +"""Schemas for the Regulatory Completeness Engine — auditable knowledge-coverage, not confidence. + +For an assessment it answers „wie sicher sind wir, dass diese Antwort VOLLSTÄNDIG ist?" by separating +IDENTIFIED regulations from ASSESSED ones (those in the validated corpus) and listing every open or +excluded domain WITH a reason. The metric is counts, never a single „87%". This is an internal quality +machine: the product never claims full coverage — it makes its own knowledge state transparent. +Deterministic, computed-not-stored, no new meta-model class (freeze v1.0). Python 3.9 compatible. +""" + +from __future__ import annotations + +from enum import Enum +from typing import List + +from pydantic import BaseModel, Field + + +class CorpusStatus(str, Enum): + """The maturity of our knowledge corpus for a regulation/domain.""" + + VALIDATED = "validated" # we can fully assess this + DRAFT = "draft" # partial / under review + UNSUPPORTED = "unsupported" # triggered but no corpus yet + UNKNOWN = "unknown" # not in our registry at all + + +class DomainCoverage(BaseModel): + regulation: str + status: CorpusStatus = CorpusStatus.UNKNOWN + note: str = "" + + +class Exclusion(BaseModel): + """A domain/regulation DELIBERATELY not assessed — always with a reason (the heart of the engine).""" + + subject: str + reason: str + deciding_question: str = "" # what would resolve it (if a query) + resolution: str = "future_corpus" # query_required | future_corpus | not_applicable + + +class Assumption(BaseModel): + key: str + value: str = "" + note: str = "" + + +class CompletenessReport(BaseModel): + """The auditable coverage report for one assessment — counts + justification, NO single percentage.""" + + identified_regulations: List[str] = Field(default_factory=list) + assessed_regulations: List[str] = Field(default_factory=list) # in the validated corpus + open_regulations: List[str] = Field(default_factory=list) # identified but not validated + open_corpora: List[str] = Field(default_factory=list) # missing domains worth building + coverage: List[DomainCoverage] = Field(default_factory=list) + assumptions: List[Assumption] = Field(default_factory=list) + exclusions: List[Exclusion] = Field(default_factory=list) + uncertainties_count: int = 0 + assessed_obligations: int = 0 # injected (Execution-owned) + justification_present: bool = False + completeness_summary: str = "" # "Identifiziert N · bewertet M · offen K · ..." + audit_statement: str = "" # the honest narrative sentence diff --git a/backend-compliance/compliance/journey_matcher/__init__.py b/backend-compliance/compliance/journey_matcher/__init__.py new file mode 100644 index 00000000..ca6c1784 --- /dev/null +++ b/backend-compliance/compliance/journey_matcher/__init__.py @@ -0,0 +1,30 @@ +"""Journey Matcher — the Delta -> Journey function of the Capability Delta Engine. + +The third independent function of the pipeline (after Company 2A `Evidence -> Capability` and RS-005 +`Capability -> Delta`): given ONLY the Capability Delta, rank the known journeys that best EXPLAIN it. +A Journey is an EXPLANATION of the delta, not its cause — order is `Goal -> Required -> Delta -> Journey`. + +Deliberately dumb + deterministic (pure set overlap; no ML/embeddings/LLM), fully auditable, signatures +INJECTED (certificate-agnostic capability clusters). No new corpus, no graph (freeze v1.0). The Matcher +is sanctioned as the last architectural building block; everything after is knowledge work. +""" + +from __future__ import annotations + +from .engine import match_journeys +from .schemas import ( + JourneyMatch, + JourneyMatchReason, + JourneyMatchResult, + JourneySignature, + MatchContext, +) + +__all__ = [ + "match_journeys", + "JourneySignature", + "MatchContext", + "JourneyMatch", + "JourneyMatchReason", + "JourneyMatchResult", +] diff --git a/backend-compliance/compliance/journey_matcher/engine.py b/backend-compliance/compliance/journey_matcher/engine.py new file mode 100644 index 00000000..445dcf2b --- /dev/null +++ b/backend-compliance/compliance/journey_matcher/engine.py @@ -0,0 +1,94 @@ +"""Journey Matcher — the Delta -> Journey function of the Capability Delta Engine. + +Three INDEPENDENT functions now compose the pipeline, each a different problem, all interchangeable: + 1. Evidence -> Capability (Company 2A) + 2. Capability -> Delta (RS-005, transition_reasoning) + 3. Delta -> Journey (THIS module) + +The paradigm shift: a Journey is no longer the CAUSE (Goal -> Journey -> Delta) but the EXPLANATION +(Goal -> Required -> Delta -> Journey). The matcher does NOT look at certifications, regulations, +tenders, OEM specs or the goal — it looks ONLY at the Capability Delta and asks: which known journeys +describe exactly this delta? Output is a ranked, auditable explanation ("Journey A explains 82% of the +delta, because 8 of 10 missing capabilities are identical, same target type, ..."). + +Deliberately DUMB and deterministic: pure set overlap, NO ML, NO embeddings, NO LLM. A learning ranker +can be layered ON TOP later; this core stays auditable. Journey signatures are INJECTED (certificate- +agnostic capability clusters), never loaded here — the engine stays hermetic. No new corpus, no +graph/meta-model class (freeze v1.0). Python 3.9 compatible. + +Honesty: `score` is the share of the DELTA a journey explains (recall over the customer's missing +capabilities), never a "fit" or a compliance verdict. `journey_only` documents where a journey reaches +BEYOND this delta, so a broad journey that explains everything is not silently preferred. +""" + +from __future__ import annotations + +from typing import List, Optional, Sequence + +from .schemas import ( + JourneyMatch, + JourneyMatchReason, + JourneyMatchResult, + JourneySignature, + MatchContext, +) + + +def _context_signals(journey: JourneySignature, context: Optional[MatchContext]) -> List[str]: + """Corroborating reasons only — these are documented, they never change the score.""" + if context is None: + return [] + signals: List[str] = [] + if context.target_type and journey.target_type and context.target_type == journey.target_type: + signals.append("gleiche Zielart") + if context.industry and journey.industry and context.industry == journey.industry: + signals.append("gleiche Branche") + if context.product_type and journey.product_type and context.product_type == journey.product_type: + signals.append("gleicher Produkttyp") + return signals + + +def match_journeys( + delta: Sequence[str], + journeys: Sequence[JourneySignature], + context: Optional[MatchContext] = None, +) -> JourneyMatchResult: + """Rank known journeys by the share of the Capability Delta they EXPLAIN. + + `delta` = the customer's MISSING capabilities (from RS-005). `journeys` = injected, certificate- + agnostic signatures. score = |delta INTERSECT pattern| / |delta|. Ranking is deterministic: + score desc, then context-signal count desc (corroboration only), then journey_id asc. Context + never changes the score — only the documented reasons. Pure; no I/O; computed-not-stored. + """ + delta_set = set(delta) + n = len(delta_set) + matches: List[JourneyMatch] = [] + for j in journeys: + pattern = set(j.capability_pattern) + matched = sorted(delta_set & pattern) + score = (len(matched) / n) if n else 0.0 + signals = _context_signals(j, context) + reason = JourneyMatchReason( + matched_capabilities=matched, + unexplained_delta=sorted(delta_set - pattern), + journey_only=sorted(pattern - delta_set), + context_signals=signals, + ) + matches.append( + JourneyMatch( + journey_id=j.journey_id, + label=j.label, + score=round(score, 2), + explains="%d von %d fehlenden Capabilities" % (len(matched), n), + reason=reason, + ) + ) + matches.sort(key=lambda m: (-m.score, -len(m.reason.context_signals), m.journey_id)) + best = matches[0] if matches and matches[0].score > 0.0 else None + headline = ( + "%d Journeys erklaeren das Delta; beste: %s (%d%% des Deltas)" + % (sum(1 for m in matches if m.score > 0.0), best.label, round(best.score * 100)) + if best + else "Keine bekannte Journey erklaert dieses Delta (neue Journey-Kandidatin)" + ) + return JourneyMatchResult(delta_size=n, matches=matches, best=best, headline=headline) diff --git a/backend-compliance/compliance/journey_matcher/schemas.py b/backend-compliance/compliance/journey_matcher/schemas.py new file mode 100644 index 00000000..83376e97 --- /dev/null +++ b/backend-compliance/compliance/journey_matcher/schemas.py @@ -0,0 +1,66 @@ +"""Schemas for the Journey Matcher — the Delta -> Journey function of the Capability Delta Engine. + +Derived views (computed-not-stored): nothing here is persisted; every match is recomputed from the +input delta + injected journey signatures each call. No new corpus, no graph (freeze v1.0). +Python 3.9 compatible (no `|` unions). +""" + +from __future__ import annotations + +from typing import List, Optional + +from pydantic import BaseModel, Field + + +class JourneySignature(BaseModel): + """A known journey described ONLY by its capability pattern (Input cluster -> Output cluster). + + Deliberately certificate-/regulation-agnostic: the match uses `capability_pattern` alone. `label` + and the context fields exist for the human-auditable explanation, NEVER for the score. (Today the + signatures are derived from the transition patterns; the IDs like "ISO27001->CRA" are just one way + to describe the clusters — the matcher never reads them.) + """ + + journey_id: str + label: str + capability_pattern: List[str] = Field(default_factory=list) # OUTPUT cluster: the delta this journey is about + assumed_capabilities: List[str] = Field(default_factory=list) # INPUT cluster: typically already present + industry: Optional[str] = None + product_type: Optional[str] = None + target_type: Optional[str] = None # context only: regulation / certification / contract / environmental + + +class MatchContext(BaseModel): + """Optional corroborating context — surfaced as documented reasons, never part of the score.""" + + industry: Optional[str] = None + product_type: Optional[str] = None + target_type: Optional[str] = None + + +class JourneyMatchReason(BaseModel): + """The auditable WHY behind one match — everything a reviewer needs, no opaque score.""" + + matched_capabilities: List[str] = Field(default_factory=list) # delta INTERSECT pattern (what it explains) + unexplained_delta: List[str] = Field(default_factory=list) # delta - pattern (what it does NOT explain) + journey_only: List[str] = Field(default_factory=list) # pattern - delta (journey covers, not needed here) + context_signals: List[str] = Field(default_factory=list) # "gleiche Zielart", "gleiche Branche", ... + + +class JourneyMatch(BaseModel): + """One known journey, ranked by how much of the delta it EXPLAINS (not how well it 'fits').""" + + journey_id: str + label: str + score: float = 0.0 # |delta INTERSECT pattern| / |delta|, 0..1: share of the delta explained + explains: str = "" # "8 von 10 fehlenden Capabilities" + reason: JourneyMatchReason + + +class JourneyMatchResult(BaseModel): + """Ranked known journeys that EXPLAIN a Capability Delta. Journey = explanation, not cause.""" + + delta_size: int = 0 + matches: List[JourneyMatch] = Field(default_factory=list) # ranked desc by score + best: Optional[JourneyMatch] = None + headline: str = "" diff --git a/backend-compliance/compliance/knowledge_intake/__init__.py b/backend-compliance/compliance/knowledge_intake/__init__.py new file mode 100644 index 00000000..0d5abf51 --- /dev/null +++ b/backend-compliance/compliance/knowledge_intake/__init__.py @@ -0,0 +1,23 @@ +"""Knowledge Intake — classify an incoming document and assess its impact on existing knowledge. + +The stage BEFORE the parser: no content extraction, only Einordnung. Intersects a document's signals +(regulations + keywords) with an index of the existing knowledge to emit a `KnowledgePackage` — which +capabilities / playbooks / patterns / reference scenarios / obligations it probably touches, whether +it is a new domain, and how much review it warrants. Deterministic, no LLM, no new corpus (freeze v1.0). +""" + +from __future__ import annotations + +from .engine import assess_document_impact, build_knowledge_index +from .schemas import ( + DocumentDescriptor, ImpactLevel, KnowledgeIndex, KnowledgePackage, +) + +__all__ = [ + "build_knowledge_index", + "assess_document_impact", + "DocumentDescriptor", + "KnowledgeIndex", + "KnowledgePackage", + "ImpactLevel", +] diff --git a/backend-compliance/compliance/knowledge_intake/engine.py b/backend-compliance/compliance/knowledge_intake/engine.py new file mode 100644 index 00000000..ac6b972c --- /dev/null +++ b/backend-compliance/compliance/knowledge_intake/engine.py @@ -0,0 +1,111 @@ +"""Knowledge Intake — classify a document and assess its impact on existing knowledge. + +The real Knowledge Production is not writing — it is TARGETED UPDATING: when 20 documents arrive, +which 5 actually change our knowledge and which 15 are ignorable? Intake answers this deterministically +by intersecting a document's signals (declared regulations + keywords) with an index of the existing +knowledge (capabilities, playbooks, transition patterns, reference scenarios, injected obligations). +It performs NO content extraction (that is the later parser stage) and uses NO LLM. + +Pipeline: Knowledge Intake -> Knowledge Package -> Parser -> Draft Generator -> Review -> Published. +Pure, deterministic, computed-not-stored. No new corpus/meta-model class (freeze v1.0). Python 3.9. +""" + +from __future__ import annotations + +from typing import Any, Dict, List, Optional, Set + +from .schemas import DocumentDescriptor, ImpactLevel, KnowledgeIndex, KnowledgePackage + + +def _targets(goal_to: Any) -> List[str]: + """Extract target regulations from a transition_goal.to (single dict OR list of targets).""" + out: List[str] = [] + items = goal_to if isinstance(goal_to, list) else [goal_to] + for it in items: + if isinstance(it, dict): + reg = it.get("regulation") or it.get("target") or it.get("framework") + if reg: + out.append(str(reg)) + return out + + +def build_knowledge_index( + patterns: List[Dict[str, Any]], + playbooks: List[Dict[str, Any]], + reference_scenarios: List[Dict[str, Any]], + obligation_index: Optional[Dict[str, List[str]]] = None, +) -> KnowledgeIndex: + """Assemble the matching index from already-loaded knowledge dicts (file I/O stays in the caller).""" + tp: Dict[str, List[str]] = {} + cap_regs: Dict[str, List[str]] = {} + for p in patterns: + pid = str(p.get("id", "")) + targets = _targets(p.get("transition_goal", {}).get("to")) + if pid: + tp[pid] = targets + for item in list(p.get("likely_covered", [])) + list(p.get("delta_requirements", [])): + cap = item.get("capability") + if not cap: + continue + regs = [str(t) for t in item.get("covers_targets", [])] or targets + cap_regs.setdefault(str(cap), []) + cap_regs[str(cap)] = sorted(set(cap_regs[str(cap)]) | set(regs)) + rts = {str(r.get("id", "")): _targets(r.get("transition_goal", {}).get("to")) for r in reference_scenarios} + rts.pop("", None) + obl = obligation_index or {} + regulations = sorted( + {t for ts in tp.values() for t in ts} + | {t for ts in rts.values() for t in ts} + | {t for ts in cap_regs.values() for t in ts} + | set(obl.keys()) + ) + return KnowledgeIndex( + regulations=regulations, capability_regulations=cap_regs, + playbook_capabilities=sorted({str(pb.get("capability_id", "")) for pb in playbooks} - {""}), + transition_patterns=tp, reference_scenarios=rts, obligation_index=dict(obl), + ) + + +def _kw_match(keywords: Set[str], capability: str) -> bool: + tokens = set(capability.lower().split("_")) + return bool(keywords & tokens) or capability.lower() in keywords + + +def assess_document_impact(descriptor: DocumentDescriptor, index: KnowledgeIndex) -> KnowledgePackage: + """Classify the document and compute which existing knowledge it probably touches, and how much.""" + doc_regs = set(descriptor.regulations) + known = set(index.regulations) + unknown = sorted(doc_regs - known) + new_domain = bool(doc_regs) and not (doc_regs & known) + kw = {k.lower() for k in descriptor.keywords} + + caps = sorted(c for c, regs in index.capability_regulations.items() if (set(regs) & doc_regs) or _kw_match(kw, c)) + playbooks = sorted(set(caps) & set(index.playbook_capabilities)) + patterns = sorted(pid for pid, regs in index.transition_patterns.items() if set(regs) & doc_regs) + scenarios = sorted(rid for rid, regs in index.reference_scenarios.items() if set(regs) & doc_regs) + obligations = sorted({o for r in doc_regs for o in index.obligation_index.get(r, [])}) + + total = len(caps) + len(playbooks) + len(patterns) + len(scenarios) + len(obligations) + if new_domain: + level, rec = ImpactLevel.NEW_DOMAIN, "Neue Domäne — Corpus-Intake nötig (kein bestehendes Wissen betroffen)." + elif total == 0: + level, rec = ImpactLevel.NONE, "Wahrscheinlich ignorierbar — betrifft keinen bekannten Wissensbaustein." + elif len(caps) >= 3 or playbooks or len(obligations) >= 5: + level, rec = ImpactLevel.HIGH, "Gezielter Review priorisieren — hoher Impact auf bestehendes Wissen." + else: + level, rec = ImpactLevel.LOW, "Gezielter Review — geringer, eingegrenzter Impact." + + summary = "Betrifft %d Capabilities, %d Playbooks, %d Patterns, %d Reference Scenarios, %d Obligations; %s." % ( + len(caps), len(playbooks), len(patterns), len(scenarios), len(obligations), + "NEUE Domäne" if new_domain else "keine neue Domäne", + ) + return KnowledgePackage( + document_id=descriptor.document_id, + classification={"regulations": sorted(doc_regs), "keywords": sorted(kw), + "document_type": [descriptor.document_type] if descriptor.document_type else []}, + new_domain=new_domain, unknown_regulations=unknown, + affected_capabilities=caps, affected_playbooks=playbooks, + affected_transition_patterns=patterns, affected_reference_scenarios=scenarios, + affected_obligations=obligations, impact_level=level, + impact_summary=summary, recommendation=rec, + ) diff --git a/backend-compliance/compliance/knowledge_intake/schemas.py b/backend-compliance/compliance/knowledge_intake/schemas.py new file mode 100644 index 00000000..9859b1e1 --- /dev/null +++ b/backend-compliance/compliance/knowledge_intake/schemas.py @@ -0,0 +1,62 @@ +"""Schemas for Knowledge Intake — classify a new document and assess its IMPACT (no extraction yet). + +Before the parser/draft stages, Intake answers „welche Teile unseres Wissensbestands sind überhaupt +betroffen?". It does NOT extract content — it only classifies the document and intersects its signals +with an index of the existing knowledge (capabilities, playbooks, transition patterns, reference +scenarios, injected obligations) to emit a `KnowledgePackage` (an impact analysis). Deterministic, +computed-not-stored, no new corpus, no new meta-model class (freeze v1.0). Python 3.9 compatible. +""" + +from __future__ import annotations + +from enum import Enum +from typing import Dict, List + +from pydantic import BaseModel, Field + + +class ImpactLevel(str, Enum): + NONE = "none" # touches nothing known -> likely ignorable + LOW = "low" # touches a little -> targeted review + HIGH = "high" # touches a lot -> prioritise review + NEW_DOMAIN = "new_domain" # references only unknown regulations -> domain intake + + +class DocumentDescriptor(BaseModel): + """Lightweight signals of an incoming document — NO content body, only classification inputs.""" + + document_id: str + title: str = "" + source: str = "" # e.g. BSI, ENISA, EU + document_type: str = "" # e.g. guidance, faq, regulation, recommendation + regulations: List[str] = Field(default_factory=list) # declared regulations it references + keywords: List[str] = Field(default_factory=list) # lightweight topic signals (e.g. sbom) + product_types: List[str] = Field(default_factory=list) + + +class KnowledgeIndex(BaseModel): + """A deterministic index of the EXISTING knowledge to match an incoming document against.""" + + regulations: List[str] = Field(default_factory=list) # all regulations the corpus knows + capability_regulations: Dict[str, List[str]] = Field(default_factory=dict) # capability -> covers_targets + playbook_capabilities: List[str] = Field(default_factory=list) # capabilities that HAVE a playbook + transition_patterns: Dict[str, List[str]] = Field(default_factory=dict) # pattern_id -> target regulations + reference_scenarios: Dict[str, List[str]] = Field(default_factory=dict) # rts_id -> regulations + obligation_index: Dict[str, List[str]] = Field(default_factory=dict) # regulation -> obligation ids (INJECTED) + + +class KnowledgePackage(BaseModel): + """The impact analysis for one document — what of our knowledge it probably touches, and how much.""" + + document_id: str + classification: Dict[str, List[str]] = Field(default_factory=dict) # echoed regulations/keywords/types + new_domain: bool = False + unknown_regulations: List[str] = Field(default_factory=list) + affected_capabilities: List[str] = Field(default_factory=list) + affected_playbooks: List[str] = Field(default_factory=list) + affected_transition_patterns: List[str] = Field(default_factory=list) + affected_reference_scenarios: List[str] = Field(default_factory=list) + affected_obligations: List[str] = Field(default_factory=list) + impact_level: ImpactLevel = ImpactLevel.NONE + impact_summary: str = "" + recommendation: str = "" diff --git a/backend-compliance/compliance/knowledge_production/__init__.py b/backend-compliance/compliance/knowledge_production/__init__.py new file mode 100644 index 00000000..0714d8e0 --- /dev/null +++ b/backend-compliance/compliance/knowledge_production/__init__.py @@ -0,0 +1,19 @@ +"""Knowledge Production — deterministically prepare the corpus, then curate it. + +The corpus is not written by hand: the Playbook Draft Generator structures drafts from data the +software already owns (Transition Pattern + leverage + injected Execution controls), leaving the +practitioner know-how as TODO for expert review. Mirrors the legal pipeline (Parser -> Review). +Deterministic, no LLM in core, no new corpus, no new meta-model class (freeze v1.0). +""" + +from __future__ import annotations + +from .engine import drafts_from_pattern, generate_playbook_draft +from .schemas import DraftStatus, PlaybookDraft + +__all__ = [ + "generate_playbook_draft", + "drafts_from_pattern", + "PlaybookDraft", + "DraftStatus", +] diff --git a/backend-compliance/compliance/knowledge_production/engine.py b/backend-compliance/compliance/knowledge_production/engine.py new file mode 100644 index 00000000..2d39a076 --- /dev/null +++ b/backend-compliance/compliance/knowledge_production/engine.py @@ -0,0 +1,91 @@ +"""Knowledge Production — the Playbook Draft Generator (deterministic assembly + expert review). + +Mirrors the legal pipeline (Gesetz -> Parser -> Obligation -> Review) for BreakPilot's OWN knowledge: +new Capability -> Registry -> Transition Pattern -> **Playbook Draft Generator** -> Expert Review -> +versioned Playbook. The generator does not WRITE playbooks — it STRUCTURES drafts from data the +software already owns (a transition/convergence pattern's delta requirement: why_asked, covers_targets, +expected_evidence) plus injected Execution controls. The practitioner know-how (tools / process steps / +how others do it) is left as an explicit TODO for the expert (or a separate offline-propose step). + +Fully deterministic, NO LLM in the core (deterministic-first: any model enrichment is offline, +advisory, never in this assembly). No new corpus, no new meta-model class (freeze v1.0). Python 3.9. +""" + +from __future__ import annotations + +from typing import Any, Dict, List, Optional + +from .schemas import DraftStatus, PlaybookDraft + +_SOFT_FIELDS = ["tools", "process_steps", "how_others_do_it"] # practitioner know-how — expert/offline-propose +_DISCLAIMER = ( + "Maschinell assemblierter ENTWURF aus vorhandenen Daten (Transition Pattern + Leverage + " + "injizierte Controls). KEINE normative Anforderung; erfordert fachliche Kuratierung (TODO-Felder) " + "und Statuswechsel draft_generated -> reviewed -> validated." +) + + +def generate_playbook_draft( + capability_id: str, + requirement: Optional[Dict[str, Any]] = None, + control_links: Optional[List[str]] = None, +) -> PlaybookDraft: + """Assemble a playbook draft for ONE capability from a pattern delta requirement (deterministic). + + `requirement`: a delta_requirement dict (why_asked / covers_targets / expected_evidence). Owned + fields are filled with provenance; soft fields are listed in `todo`. `control_links`: injected + Execution controls (default empty — no Execution data in the draft generator). + """ + req = requirement or {} + why = str(req.get("why_asked") or req.get("missing_because") or "") + closes = sorted({str(t) for t in req.get("covers_targets", [])}) + evidence = [str(e) for e in req.get("expected_evidence", [])] + controls = list(control_links or []) + + provenance: Dict[str, str] = {} + todo: List[str] = [] + if why: + provenance["why"] = "transition_pattern:why_asked" + else: + todo.append("why") + if closes: + provenance["closes_regulations"] = "leverage:covers_targets" + if evidence: + provenance["expected_evidence"] = "transition_pattern:expected_evidence" + else: + todo.append("expected_evidence") + if controls: + provenance["typical_controls"] = "execution:control_links" + todo.extend(_SOFT_FIELDS) # always expert-owned + + return PlaybookDraft( + capability_id=capability_id, + status=DraftStatus.DRAFT_GENERATED, + title=capability_id.replace("_", " "), + why=why, + closes_regulations=closes, + expected_evidence=evidence, + typical_controls=controls, + provenance=provenance, + todo=todo, + disclaimer=_DISCLAIMER, + ) + + +def drafts_from_pattern( + pattern: Dict[str, Any], + control_links_by_cap: Optional[Dict[str, List[str]]] = None, +) -> List[PlaybookDraft]: + """Assemble one playbook draft per delta capability of a transition/convergence pattern. + + This is the "produce drafts, don't write them" tool: feed a pattern -> get a draft per missing + capability, ready for expert review. Deterministic + order-preserving (pattern order). + """ + links = control_links_by_cap or {} + drafts: List[PlaybookDraft] = [] + for d in pattern.get("delta_requirements", []): + cap = d.get("capability") + if not cap: + continue + drafts.append(generate_playbook_draft(str(cap), d, links.get(str(cap)))) + return drafts diff --git a/backend-compliance/compliance/knowledge_production/schemas.py b/backend-compliance/compliance/knowledge_production/schemas.py new file mode 100644 index 00000000..fb6c944a --- /dev/null +++ b/backend-compliance/compliance/knowledge_production/schemas.py @@ -0,0 +1,46 @@ +"""Schemas for Knowledge Production — deterministic draft assembly + lifecycle. + +The corpus is no longer written by hand: it is deterministically PREPARED from data the software +already owns (Capability, Transition Pattern, Controls, Evidence, leverage), then curated by an +expert. A `PlaybookDraft` is a machine-assembled skeleton with per-field provenance and an explicit +TODO list of what still needs human (or offline-propose) input. No LLM in the deterministic core. +Python 3.9 compatible (no `|` unions). +""" + +from __future__ import annotations + +from enum import Enum +from typing import Dict, List + +from pydantic import BaseModel, Field + + +class DraftStatus(str, Enum): + """Freigabestatus — the knowledge lifecycle from machine draft to proven (mirrors the + transition-pattern / playbook maturity, with a machine-assembled pre-stage).""" + + DRAFT_GENERATED = "draft_generated" # machine-assembled, NOT yet expert-touched + IN_REVIEW = "in_review" # an expert is curating it + REVIEWED = "reviewed" # internally reviewed + VALIDATED = "validated" # domain expert confirmed + PROVEN = "proven" # confirmed in the field + + +class PlaybookDraft(BaseModel): + """A deterministically assembled playbook draft for one capability. + + Owned fields (why / closes_regulations / expected_evidence / typical_controls) are filled from + existing data with provenance; the practitioner know-how (tools / process_steps / how_others) + is left as TODO. The expert reviews a draft instead of writing from a blank page. + """ + + capability_id: str + status: DraftStatus = DraftStatus.DRAFT_GENERATED + title: str = "" + why: str = "" # from the transition pattern (why_asked/missing_because) + closes_regulations: List[str] = Field(default_factory=list) # from leverage (covers_targets) + expected_evidence: List[str] = Field(default_factory=list) # from the transition pattern + typical_controls: List[str] = Field(default_factory=list) # injected from Execution (may be empty) + provenance: Dict[str, str] = Field(default_factory=dict) # field -> source it was assembled from + todo: List[str] = Field(default_factory=list) # fields the expert/offline-propose must still add + disclaimer: str = "" # machine draft, requires expert curation diff --git a/backend-compliance/compliance/onboarding/__init__.py b/backend-compliance/compliance/onboarding/__init__.py new file mode 100644 index 00000000..be29e32c --- /dev/null +++ b/backend-compliance/compliance/onboarding/__init__.py @@ -0,0 +1,86 @@ +"""Smart Onboarding Advisor — the onboarding runtime step (orchestration over existing engines). + +Turns (company + products + certifications + target) into inferred assumptions, the next best questions +(<=5, each self-explaining), the capability delta, top measures, evidence requests and completeness — +with NO sales interpretation and NO regulation picking. Orchestrator only: no new engine/registry/ +meta-model; certificate->capability hypotheses and target requirements are INJECTED. +""" + +from __future__ import annotations + +from .engine import advisor_start, apply_answer +from .hypotheses import ( + CapabilityHypothesis, + inferred_hypotheses, + resolve_for_certifications, +) +from .observations import ( + Observation, + ObservationType, + empirical_confidence, + empirical_distribution, + reviewed, +) +from .observation_log import ( + HypothesisStats, + ObservationRecord, + aggregate_by_hypothesis, + append_observation, + load_observations, + review_queue, +) +from .signals import ( + ProducedSignal, + SignalVocabularyEntry, + normalize_signals, +) +from .silent_intake import ( + DetectedCapability, + IntakeSignal, + ProductFact, + SignalMapping, + SilentIntakeResult, + silent_intake, +) +from .schemas import ( + AdvisorMeasure, + AdvisorQuestion, + AdvisorResult, + InferredAssumption, + OnboardingInput, + RejectedAssumption, +) + +__all__ = [ + "advisor_start", + "apply_answer", + "OnboardingInput", + "AdvisorResult", + "AdvisorQuestion", + "AdvisorMeasure", + "InferredAssumption", + "RejectedAssumption", + "CapabilityHypothesis", + "inferred_hypotheses", + "resolve_for_certifications", + "Observation", + "ObservationType", + "empirical_distribution", + "empirical_confidence", + "reviewed", + "silent_intake", + "IntakeSignal", + "SignalMapping", + "DetectedCapability", + "ProductFact", + "SilentIntakeResult", + "ProducedSignal", + "SignalVocabularyEntry", + "normalize_signals", + "ObservationRecord", + "HypothesisStats", + "append_observation", + "load_observations", + "aggregate_by_hypothesis", + "review_queue", +] diff --git a/backend-compliance/compliance/onboarding/engine.py b/backend-compliance/compliance/onboarding/engine.py new file mode 100644 index 00000000..5ad2a67f --- /dev/null +++ b/backend-compliance/compliance/onboarding/engine.py @@ -0,0 +1,159 @@ +"""Smart Onboarding Advisor — orchestration over the existing engines (the onboarding runtime step). + +The point of the whole platform, made usable: the user types company + products + certifications + +target, and the system does the rest — no sales interpretation, no regulation picking. This is an +ORCHESTRATOR, not a new engine: it wires Company 2A (Evidence -> Capability), RS-005 (Capability -> +Delta), optimization (Delta -> Roadmap) and completeness into one onboarding flow. + +Three principles it must honour (acceptance criteria): + - Multi-cert works; a profile is built from ALL certificates. + - relevance(evidence, target): ISO 14001 is NOT falsely relevant to the CRA; ISO 27001/TISAX REDUCE + questions but satisfy NOTHING automatically (Welt-1 -> verification_required). + - Only the NEXT BEST questions (<= 5), each explaining WHY; every answer updates the profile. + +Certificate -> probable-capability hypotheses and the target's required capabilities are INJECTED (the +hypotheses are curated knowledge, not in this code). No corpus loaded here. Python 3.9 compatible. +""" + +from __future__ import annotations + +from typing import Dict, List, Optional, Sequence + +from ..company import ( + CapabilityMappingEntry, + Certification, + CompanyCapabilityProfile, + CompanyContext, + build_company_profile, +) +from ..completeness import assess_completeness +from ..optimization import roadmap_from_delta +from ..reasoning.enums import Confidence +from ..transition_reasoning import ( + CoverageStatus, + TargetRequirement, + TransitionContext, + TransitionGoal, + assess_transition, +) +from .schemas import ( + AdvisorMeasure, + AdvisorQuestion, + AdvisorResult, + InferredAssumption, + OnboardingInput, + RejectedAssumption, +) + +_GAIN = {"high": 3, "medium": 2, "low": 1} +_RISK = {"high": 2, "medium": 1, "low": 0} + + +def _profile( + inp: OnboardingInput, cert_hypotheses: Dict[str, List[str]], + detected: Optional[Sequence[str]] = None, +) -> CompanyCapabilityProfile: + cmap = { + cert: CapabilityMappingEntry(capability_ids=list(caps), confidence=Confidence.MEDIUM) + for cert, caps in cert_hypotheses.items() + if cert in inp.certifications and caps + } + certs = [Certification(certification_id=c) for c in cmap] + if detected: # Silent Pass: concrete findings -> HIGH confidence + cmap["__detected__"] = CapabilityMappingEntry( + capability_ids=list(dict.fromkeys(detected)), confidence=Confidence.HIGH) + certs.append(Certification(certification_id="__detected__")) + return build_company_profile(CompanyContext(company_id=inp.company or "company", certifications=certs), cmap) + + +def advisor_start( + inp: OnboardingInput, + cert_hypotheses: Dict[str, List[str]], + target_requirements: Sequence[TargetRequirement], + target_id: str = "target", + covers_targets: Optional[Dict[str, List[str]]] = None, + corpus_status: Optional[Dict[str, str]] = None, + uncertain: Optional[List[Dict[str, str]]] = None, + detected_capabilities: Optional[Sequence[str]] = None, + indicative_capabilities: Optional[Sequence[str]] = None, +) -> AdvisorResult: + """Run the onboarding flow: (silent intake +) certs -> profile -> delta -> ranked questions + measures. + + Pure orchestration; deterministic. `cert_hypotheses` (cert -> probable cap ids), `target_requirements` + and `detected_capabilities` (from the Silent Knowledge Pass) are INJECTED. Detected capabilities are + recognised WITHOUT asking -> they shrink the delta and remove questions. + """ + covers_targets = covers_targets or {} + required = {r.capability_id for r in target_requirements} + profile = _profile(inp, cert_hypotheses, detected_capabilities) + auto_detected = sorted(set(detected_capabilities or []) & required) + # partial/indicative signals raise assumption strength but are NOT fed into the profile -> the gap + # stays open and is still asked. Surface only those still relevant and NOT already auto-detected. + indications = sorted((set(indicative_capabilities or []) & required) - set(auto_detected)) + assess = assess_transition( + TransitionContext(company_id=inp.company or "company", target=TransitionGoal(target_id=target_id)), + list(target_requirements), profile) + + # inferred (Welt-1): per cert, the caps it probably provides that are RELEVANT to this target + inferred: List[InferredAssumption] = [] + rejected: List[RejectedAssumption] = [] + for cert in inp.certifications: + caps = set(cert_hypotheses.get(cert, [])) + relevant = sorted(caps & required) + if relevant: + inferred.append(InferredAssumption( + certification=cert, capabilities=relevant, + statement="%s legt %d relevante Fähigkeit(en) nahe — Verifikation erforderlich, nicht automatisch erfüllt" + % (cert, len(relevant)))) + elif caps: + rejected.append(RejectedAssumption( + certification=cert, + statement="%s ist für dieses Ziel nicht relevant" % cert, + reason="relevance(evidence, target) = 0 — keine geforderte Fähigkeit abgedeckt")) + + # next best questions (<=5): re-rank the RS-005 requests by info gain + leverage + risk + evidence-gap + known_ev = set(inp.known_evidence) + scored = [] + for q in assess.question_requests: + lev = len(covers_targets.get(q.capability_id, [])) + ev_missing = 1 if (q.expected_evidence and not (set(q.expected_evidence) & known_ev)) else 0 + score = _GAIN.get(q.information_gain.value, 1) + lev + _RISK.get(q.priority.value, 0) + ev_missing + scored.append((score, q)) + scored.sort(key=lambda x: (-x[0], x[1].capability_id)) + next_q = [ + AdvisorQuestion(capability_id=q.capability_id, question_intent=q.question_intent, why=q.reason, + information_value=float(s), priority=q.priority.value) + for s, q in scored[:5] + ] + + delta = sorted({c.capability_id for c in assess.coverage if c.status == CoverageStatus.MISSING}) + plan = roadmap_from_delta(assess, {c: covers_targets.get(c, []) for c in delta}) + measures = [AdvisorMeasure(capability_id=m.capability_id, leverage=m.leverage, closes=m.covers) + for m in plan.ranked_measures[:5]] + evidence = sorted({e for q in assess.question_requests for e in q.expected_evidence}) + + applicable = list(inp.target) or [target_id] + rep = assess_completeness(applicable, corpus_status or {}, uncertain=uncertain or []) + unsupported = [e.subject for e in rep.exclusions] + + probably = [c for c in assess.summary.probably_covered if c not in set(auto_detected)] + return AdvisorResult( + inferred_assumptions=inferred, rejected_assumptions=rejected, auto_detected=auto_detected, + indications=indications, + next_best_questions=next_q, capability_delta=delta, top_measures=measures, + evidence_requests=evidence, unsupported_domains=unsupported, + completeness_summary=rep.completeness_summary, + headline="%d von %d Anforderungen offen · %d automatisch erkannt (Intake) · %d wahrscheinlich (Zertifikate) · %d zu klären" + % (len(delta), len(assess.coverage), len(auto_detected), len(probably), len(next_q))) + + +def apply_answer(known_capabilities: Sequence[str], capability_id: str, answer: str) -> List[str]: + """Update the known-capability set from one answer. `answer` in {confirmed, rejected, unknown}. + + A confirmed answer adds the capability to the known set (shrinking the delta on the next run); + rejected/unknown leave it open. This is how every answer updates the profile (criterion 6). + """ + known = list(dict.fromkeys(known_capabilities)) + if answer == "confirmed" and capability_id not in known: + known.append(capability_id) + return known diff --git a/backend-compliance/compliance/onboarding/hypotheses.py b/backend-compliance/compliance/onboarding/hypotheses.py new file mode 100644 index 00000000..31687e16 --- /dev/null +++ b/backend-compliance/compliance/onboarding/hypotheses.py @@ -0,0 +1,54 @@ +"""Certification Capability Hypotheses — capability-centric, with EMPIRICAL (computed) confidence. + +Each hypothesis is its own knowledge object: "IF a company holds one of `supported_by` certs, we EXPECT +`capability` (verification required)" — Welt-1, never "erfüllt". Written ONCE per capability with a list +of supporting certs (reuse, not redundancy), so multi-certification merges AUTOMATICALLY. + +`confidence` is NOT an expert/LLM score: it is COMPUTED from real-onboarding observations +(confirmed / (confirmed+refuted)), `None` until any are seen. This is the empirical learning loop — the +long-term moat. The library is DATA, loaded outside this module and injected. Python 3.9 compatible. +""" + +from __future__ import annotations + +from typing import Dict, List, Sequence + +from pydantic import BaseModel, Field + + +class CapabilityHypothesis(BaseModel): + """Curated knowledge only. Confidence is NOT stored here — it is computed from the reviewed + observation stream (see observations.py); a raw answer never changes a hypothesis (review gate).""" + + id: str + capability: str + supported_by: List[str] = Field(default_factory=list) # certifications that suggest this capability + relationship: str = "supports" # supports / partially_supports + verification_required: bool = True # Welt-1: never auto-satisfied + question_intent: str = "verify_existence" + expected_evidence: List[str] = Field(default_factory=list) + kind: str = "shared" # shared / specific + + +def inferred_hypotheses( + certifications: Sequence[str], library: Sequence[CapabilityHypothesis] +) -> List[CapabilityHypothesis]: + """Every hypothesis whose `supported_by` intersects the company's certs — the auto multi-cert merge.""" + certs = set(certifications) + return [h for h in library if certs & set(h.supported_by)] + + +def resolve_for_certifications( + certifications: Sequence[str], library: Sequence[CapabilityHypothesis] +) -> Dict[str, List[str]]: + """Adapt the capability-centric library to the Advisor's `cert -> [capability]` input. + + For each held certification, the capabilities its hypotheses suggest (deduped, deterministic order). + """ + certs = set(certifications) + out: Dict[str, List[str]] = {} + for h in library: + for cert in h.supported_by: + if cert in certs and h.capability not in out.setdefault(cert, []): + out[cert].append(h.capability) + return {c: out[c] for c in sorted(out)} diff --git a/backend-compliance/compliance/onboarding/observation_log.py b/backend-compliance/compliance/onboarding/observation_log.py new file mode 100644 index 00000000..06c7f45d --- /dev/null +++ b/backend-compliance/compliance/onboarding/observation_log.py @@ -0,0 +1,108 @@ +"""Observation Log — append-only JSONL store for empirical calibration events (Task 59b v1). + +Observations are NOT business data and NOT product-DB data — they are CALIBRATION events for the +knowledge base ("ISO27001 -> SDL confirmed", "TISAX -> supplier security refuted"). So they live with the +other versioned knowledge artifacts (hypotheses, transition patterns, vocabulary), NOT in the product +database: an append-only JSONL log under `knowledge/observations/`. NO migration, NO DB. The empirical +DISTRIBUTION and CONFIDENCE are COMPUTED from this log on demand (computed-not-stored) — a hypothesis is +NEVER auto-updated; only REVIEWED observations calibrate (the review gate, enforced in observations.py). + +Append-only: each line is one ObservationRecord and lines are NEVER modified in place. A later review is +a NEW line with the same observation_id and reviewed=true; load_observations() reconciles to the latest +per id. You can `rm` the log and recompute, `git diff` it over months, or rebuild confidence under a new +policy. Anonymisation is MANDATORY: customer_archetype is a sector/cert archetype, NEVER a real company +name (this file is committed to git). Time is stamped by the CALLER (no hidden clock) for determinism. +I/O only at the append/load boundary; statistics are pure. Python 3.9 compatible. +""" + +from __future__ import annotations + +import json +import os +from typing import Dict, List, Optional, Sequence + +from pydantic import BaseModel, Field + +from .observations import Observation, empirical_confidence, empirical_distribution + +_DEFAULT_LOG = os.path.join( + os.path.dirname(__file__), "..", "..", "knowledge", "observations", "observations.jsonl") + + +class ObservationRecord(Observation): + """A persisted observation line: an Observation (with its review gate + observation_type) plus log + metadata. `observation_id` is stable — a review re-appends the SAME id with reviewed=true.""" + + observation_id: str # stable id; a review re-appends the same id + timestamp: str = "" # ISO 8601, stamped by the CALLER (no hidden clock) + customer_archetype: str = "" # sector/cert archetype — NEVER a real company name + evidence: str = "" # what backs the answer (reference, not the artifact) + provenance: str = "" # where the answer came from (audit trail) + knowledge_version: str = "" # hypotheses/vocabulary version observed under + + +class HypothesisStats(BaseModel): + """Per-hypothesis empirical rollup — all COMPUTED from the log, nothing stored on the hypothesis.""" + + hypothesis_id: str + distribution: Dict[str, int] = Field(default_factory=dict) # reviewed counts per observation_type + confidence: Optional[float] = None # None until a for/against obs is reviewed + reviewed_count: int = 0 + total_count: int = 0 + + +def append_observation(record: ObservationRecord, path: str = _DEFAULT_LOG) -> None: + """Append ONE record as a JSON line. Append-only — existing lines are never rewritten.""" + os.makedirs(os.path.dirname(path), exist_ok=True) + line = json.dumps(record.model_dump(mode="json"), ensure_ascii=False, sort_keys=True) + with open(path, "a", encoding="utf-8") as fh: + fh.write(line + "\n") + + +def load_observations(path: str = _DEFAULT_LOG, reconcile: bool = True) -> List[ObservationRecord]: + """Read all records — a single `.jsonl` file or a directory of monthly `.jsonl` files. With + reconcile, the LATEST record per observation_id wins (a later reviewed=true supersedes the original). + Returns deterministic order (by observation_id when reconciled, else append order).""" + files: List[str] = [] + if os.path.isdir(path): + files = sorted(os.path.join(path, f) for f in os.listdir(path) if f.endswith(".jsonl")) + elif os.path.exists(path): + files = [path] + records: List[ObservationRecord] = [] + for fpath in files: + with open(fpath, encoding="utf-8") as fh: + for raw in fh: + raw = raw.strip() + if raw: + records.append(ObservationRecord(**json.loads(raw))) + if not reconcile: + return records + latest: Dict[str, ObservationRecord] = {} + for r in records: # file/append order -> later lines win + latest[r.observation_id] = r + return [latest[k] for k in sorted(latest)] + + +def aggregate_by_hypothesis(records: Sequence[ObservationRecord]) -> List[HypothesisStats]: + """Per-hypothesis distribution + confidence. The review gate applies inside empirical_distribution/ + empirical_confidence (reviewed-only), so unreviewed observations are counted in total but never + calibrate. Deterministic order (by hypothesis id).""" + by_hyp: Dict[str, List[ObservationRecord]] = {} + for r in records: + by_hyp.setdefault(r.hypothesis_id, []).append(r) + out: List[HypothesisStats] = [] + for hyp in sorted(by_hyp): + obs = by_hyp[hyp] + out.append(HypothesisStats( + hypothesis_id=hyp, + distribution=empirical_distribution(obs), # reviewed-only (the gate) + confidence=empirical_confidence(obs), # None until reviewed for/against + reviewed_count=sum(1 for o in obs if o.reviewed), + total_count=len(obs))) + return out + + +def review_queue(records: Sequence[ObservationRecord]) -> List[ObservationRecord]: + """The reviewer's worklist: observations not yet reviewed. Calibration ignores these until a reviewer + accepts them (Observation -> Review -> Accepted -> Knowledge recomputed), never Observation -> conf++.""" + return [r for r in records if not r.reviewed] diff --git a/backend-compliance/compliance/onboarding/observations.py b/backend-compliance/compliance/onboarding/observations.py new file mode 100644 index 00000000..37a411a9 --- /dev/null +++ b/backend-compliance/compliance/onboarding/observations.py @@ -0,0 +1,85 @@ +"""Observation Model — the empirical learning unit (Task 59a: model BEFORE persistence/API). + +The learning point is NOT the hypothesis, it is the QUESTION. A hypothesis ("ISO 27001 suggests supplier +management") produces a question ("Is there a documented supplier-security process?"), and the answer is +rarely binary — "yes" / "no" / "partial, only critical suppliers" / "certified but not lived" are very +different observations. So the chain is: + + Hypothesis -> Question -> Observation -> (Review) -> Hypothesis + +Two principles (durable): + - Richer than confirmed/refuted: an Observation carries an `observation_type` (confirmed / partial / + refuted / not_applicable / unknown), a free-text answer, a scope_note ("only critical suppliers"), + and whether evidence was uploaded. + - REVIEW GATE: a raw answer NEVER changes a hypothesis directly. Only REVIEWED observations calibrate; + otherwise the system learns from outliers. Hypotheses stay curated knowledge; confidence is COMPUTED + from the reviewed observation stream (keyed by hypothesis id), not stored on the hypothesis. + +This module defines the model + the deterministic statistics it enables (a DISTRIBUTION, not a single +%). Persistence (store), aggregation across customers and hypothesis calibration are later tasks +(59b/c/d). Pure, no I/O. Python 3.9 compatible. +""" + +from __future__ import annotations + +from enum import Enum +from typing import Dict, List, Optional, Sequence + +from pydantic import BaseModel, Field + + +class ObservationType(str, Enum): + CONFIRMED = "confirmed" + PARTIAL = "partial" + REFUTED = "refuted" + NOT_APPLICABLE = "not_applicable" + UNKNOWN = "unknown" + + +class Observation(BaseModel): + """One real-onboarding answer to one hypothesis-driven question. The raw empirical unit.""" + + hypothesis_id: str + capability: str = "" # denormalised for convenient aggregation + question: str = "" # the question that was actually asked + answer: str = "" # the customer's raw answer (free text) + observation_type: ObservationType = ObservationType.UNKNOWN + scope_note: Optional[str] = None # "only critical suppliers" / "only DE" / "not lived" + evidence_uploaded: bool = False + reviewed: bool = False # the review gate: only reviewed obs calibrate + reviewed_by: Optional[str] = None + + +# observation types that count as evidence for/against the capability (n/a + unknown do not) +_FOR_AGAINST = (ObservationType.CONFIRMED, ObservationType.PARTIAL, ObservationType.REFUTED) + + +def empirical_distribution( + observations: Sequence[Observation], reviewed_only: bool = True +) -> Dict[str, int]: + """Count observations per type — the DISTRIBUTION (e.g. confirmed 61 / partial 31 / refuted 8), + far richer than a single percentage. By default only REVIEWED observations count (the review gate).""" + dist = {t.value: 0 for t in ObservationType} + for o in observations: + if o.reviewed or not reviewed_only: + dist[o.observation_type.value] += 1 + return dist + + +def empirical_confidence( + observations: Sequence[Observation], reviewed_only: bool = True +) -> Optional[float]: + """Confidence from the reviewed stream: (confirmed + 0.5*partial) / (confirmed+partial+refuted). + + `not_applicable` and `unknown` are excluded from the denominator (they are not evidence either way). + `None` until any for/against observation is reviewed — never an expert/LLM score.""" + dist = empirical_distribution(observations, reviewed_only) + base = dist[ObservationType.CONFIRMED.value] + dist[ObservationType.PARTIAL.value] + dist[ObservationType.REFUTED.value] + if base == 0: + return None + return round((dist[ObservationType.CONFIRMED.value] + 0.5 * dist[ObservationType.PARTIAL.value]) / base, 2) + + +def reviewed(observations: Sequence[Observation]) -> List[Observation]: + """The calibration set: only reviewed observations (a raw answer never updates a hypothesis).""" + return [o for o in observations if o.reviewed] diff --git a/backend-compliance/compliance/onboarding/schemas.py b/backend-compliance/compliance/onboarding/schemas.py new file mode 100644 index 00000000..78c6bc3f --- /dev/null +++ b/backend-compliance/compliance/onboarding/schemas.py @@ -0,0 +1,64 @@ +"""Schemas for the Smart Onboarding Advisor — the onboarding RUNTIME step. + +DTOs only. The Advisor ORCHESTRATES the existing engines (Company 2A, RS-005, optimization, +completeness) — no new reasoning engine, no new capability registry, no new meta-model. Welt-1 +discipline: a certificate yields PROBABLE capabilities (verification required), never "erfüllt". +Python 3.9 compatible (no `|` unions). +""" + +from __future__ import annotations + +from typing import List, Optional + +from pydantic import BaseModel, Field + + +class OnboardingInput(BaseModel): + company: str = "" + industry: Optional[str] = None + products: List[str] = Field(default_factory=list) + markets: List[str] = Field(default_factory=list) + certifications: List[str] = Field(default_factory=list) + known_evidence: List[str] = Field(default_factory=list) + target: List[str] = Field(default_factory=list) # informational; the delta uses injected requirements + + +class InferredAssumption(BaseModel): + certification: str + capabilities: List[str] = Field(default_factory=list) # RELEVANT-to-target caps the cert probably provides + verification_required: bool = True # Welt-1: never auto-satisfied + statement: str = "" + + +class RejectedAssumption(BaseModel): + certification: Optional[str] = None + statement: str = "" + reason: str = "" # e.g. "relevance(evidence, target) = 0" + + +class AdvisorQuestion(BaseModel): + capability_id: str + question_intent: str + why: str # every question explains itself + information_value: float = 0.0 # deterministic rank score + priority: str = "medium" + + +class AdvisorMeasure(BaseModel): + capability_id: str + leverage: int = 0 + closes: List[str] = Field(default_factory=list) + + +class AdvisorResult(BaseModel): + inferred_assumptions: List[InferredAssumption] = Field(default_factory=list) + rejected_assumptions: List[RejectedAssumption] = Field(default_factory=list) + auto_detected: List[str] = Field(default_factory=list) # detected (concrete artifact): recognised w/o asking + indications: List[str] = Field(default_factory=list) # partial signal: raises assumption strength, STILL asked + next_best_questions: List[AdvisorQuestion] = Field(default_factory=list) # max 5 + capability_delta: List[str] = Field(default_factory=list) + top_measures: List[AdvisorMeasure] = Field(default_factory=list) + evidence_requests: List[str] = Field(default_factory=list) + unsupported_domains: List[str] = Field(default_factory=list) + completeness_summary: str = "" + headline: str = "" # "N erkannt, M wahrscheinlich abgedeckt, K zu klären" diff --git a/backend-compliance/compliance/onboarding/signals.py b/backend-compliance/compliance/onboarding/signals.py new file mode 100644 index 00000000..e982d31a --- /dev/null +++ b/backend-compliance/compliance/onboarding/signals.py @@ -0,0 +1,73 @@ +"""Signal Producer interface + Normalizer — one signal language, but TWO signal KINDS. + +The platform already HAS scanners (website, repo/code, SBOM, security headers, TLS, SPF/DKIM/DMARC, +document analysis, RAG over uploads, product classification). The Silent Pass does not want a +WebsiteScanner or a RepoScanner — it wants their UNIFIED output. So every source (a scanner, a PDF +parser, a tender parser, an OEM spec, an API, or the user) emits the SAME `ProducedSignal` +{signal_id, source_type, kind, confidence, evidence, provenance}, and `normalize_signals` reduces +producer-specific ids to ONE canonical signal via a vocabulary (id + aliases + kind) — exactly the +Requirement-Source / MCAP / regulation-alias pattern. The Silent Pass then never gets per-scanner logic. + +CRITICAL — a signal is one of two KINDS, and they NEVER substitute for each other: + observation = "I SAW X" — a repo with an SBOM, a published security.txt, a risk-assessment PDF. + requirement = "someone DEMANDS X" — a tender clause `requires_sbom`, an OEM spec `supplier_requires_psirt`. +A demanded SBOM is NOT a present SBOM. `kind` is carried on the canonical VOCABULARY entry (authoritative), +so even a mislabelled producer signal cannot collapse the two. The Silent Pass consumes ONLY observations; +requirement signals are preserved and feed the required-set / prioritisation later. This Observation-vs- +Requirement split is the very one the Requirements Verification Platform rests on: Observations (reality) +vs Requirements (targets); their comparison IS the delta. Pure, deterministic, no I/O. Python 3.9 compatible. +""" + +from __future__ import annotations + +from typing import Dict, List, Optional, Sequence + +from pydantic import BaseModel, Field + +from .silent_intake import IntakeSignal + + +class ProducedSignal(BaseModel): + """What ANY signal producer emits — the common interface every source agrees on.""" + + signal_id: str # raw or canonical id the producer used + source_type: str = "" # website / repository / document / product / tender / oem / user / api + kind: str = "" # "observation" | "requirement"; empty -> resolved from the vocabulary + confidence: float = 1.0 + evidence: Optional[str] = None # the artifact found (already in hand) + provenance: str = "" # url / filename / tender clause / "customer statement" + + +class SignalVocabularyEntry(BaseModel): + """One canonical signal + its aliases + its KIND (the authoritative observation/requirement label).""" + + id: str + kind: str = "observation" # "observation" (I saw X) | "requirement" (someone DEMANDS X) + aliases: List[str] = Field(default_factory=list) + + +def normalize_signals( + produced: Sequence[ProducedSignal], vocabulary: Sequence[SignalVocabularyEntry] +) -> List[IntakeSignal]: + """Reduce heterogeneous producer signals to the canonical IntakeSignal stream (alias resolution). + + The canonical vocabulary entry's `kind` is AUTHORITATIVE — a producer cannot relabel a requirement as + an observation (that is what stops a demanded SBOM from masquerading as a present one). Unknown signal + ids pass through unchanged (a new producer's signal stays visible, not silently dropped) and keep the + producer-declared kind (default observation). Deterministic; carries confidence/evidence/provenance. + """ + alias: Dict[str, str] = {} + kind_of: Dict[str, str] = {} + for v in vocabulary: + alias[v.id] = v.id + kind_of[v.id] = v.kind + for a in v.aliases: + alias[a] = v.id + out: List[IntakeSignal] = [] + for p in produced: + canonical = alias.get(p.signal_id, p.signal_id) + kind = kind_of.get(canonical) or p.kind or "observation" + out.append(IntakeSignal( + source=p.source_type, signal=canonical, kind=kind, confidence=p.confidence, + evidence=p.evidence, provenance=p.provenance)) + return out diff --git a/backend-compliance/compliance/onboarding/silent_intake.py b/backend-compliance/compliance/onboarding/silent_intake.py new file mode 100644 index 00000000..c3d3055b --- /dev/null +++ b/backend-compliance/compliance/onboarding/silent_intake.py @@ -0,0 +1,124 @@ +"""Silent Knowledge Pass — recognise everything possible BEFORE asking a single question (Phase 0). + +The Advisor can say "I need 5 answers" but does not yet decide WHAT it can find out by itself. The Silent +Pass runs first: from signals that existing scanners/parsers already produce (website, repository, +documents, product data) it deterministically derives capabilities the company demonstrably HAS and +product facts that drive scope — so every recognised item shrinks the delta and removes a question. + +The customer then experiences "we already recognised 11 of 17 — only these 4 remain" instead of a +question wall. This is NOT new architecture: it is one orchestration step in front of the Advisor + Company -> Silent Intake -> Company Profile -> Hypotheses -> Delta -> Top Questions +All building blocks already exist. SIGNALS are INJECTED (the scanners produce them); the signal->capability +map is curated DATA, also injected. Pure, deterministic, no I/O. Python 3.9 compatible. +""" + +from __future__ import annotations + +from typing import Dict, List, Optional, Sequence, Set + +from pydantic import BaseModel, Field + + +class IntakeSignal(BaseModel): + """A CANONICAL signal the Silent Pass consumes. Producer-agnostic: the same `signal` may have come + from a website, a repo, a PDF, a tender or the user — normalize_signals() unified them (see signals.py).""" + + source: str # source_type: website / repository / document / product / tender / user + signal: str # CANONICAL signal id, e.g. "sbom_present" + kind: str = "observation" # "observation" (I saw X) | "requirement" (someone DEMANDS X) + confidence: float = 1.0 # carried from the producer + evidence: Optional[str] = None # the artifact already in hand + provenance: str = "" # where it came from (url / filename / tender clause) — audit trail + detail: str = "" # free-text (kept for back-compat) + + +class SignalMapping(BaseModel): + """Curated: what a signal lets us conclude. A signal yields a capability OR a product fact.""" + + signal: str + capability: Optional[str] = None # capability the signal evidences + relationship: str = "detected" # detected (concrete artifact) / partial (indicative) + evidence: Optional[str] = None # the artifact found (already in hand -> no upload needed) + product_fact: Optional[str] = None # e.g. "connected_to_internet" + fact_value: str = "true" + rationale: str = "" # curated note: WHY only indicative (esp. for partial mappings) + + +class DetectedCapability(BaseModel): + capability: str + relationship: str = "detected" + source: str = "" # which signal/source detected it (audit trail) + evidence: Optional[str] = None + confidence: float = 1.0 # carried from the producing signal + provenance: str = "" # where the signal came from + + +class ProductFact(BaseModel): + key: str + value: str = "true" + source: str = "" + + +class SilentIntakeResult(BaseModel): + detected_capabilities: List[DetectedCapability] = Field(default_factory=list) + product_facts: List[ProductFact] = Field(default_factory=list) + evidence_found: List[str] = Field(default_factory=list) + requirements_seen: List[str] = Field(default_factory=list) # requirement-kind signals — preserved, NOT present + summary: str = "" + + def capability_ids(self) -> List[str]: + """The DETECTED capability ids (relationship == detected) — fed into the Advisor as already-present + (delta-reducing, not asked). ONLY observation-kind signals reach here (requirements never become a + present capability); a merely PARTIAL/indicative signal does NOT (see indicative_capability_ids).""" + return sorted({d.capability for d in self.detected_capabilities if d.relationship == "detected"}) + + def indicative_capability_ids(self) -> List[str]: + """Capabilities backed only by a PARTIAL/indicative signal — they raise assumption strength but do + NOT replace a question (the gap stays open and is still asked, just with an indication shown).""" + return sorted({d.capability for d in self.detected_capabilities if d.relationship != "detected"}) + + +def silent_intake( + signals: Sequence[IntakeSignal], signal_map: Sequence[SignalMapping] +) -> SilentIntakeResult: + """Derive capabilities + product facts from injected scanner signals (deterministic, no questions). + + Each signal is matched to curated mappings by `signal` id; a mapping contributes either a detected + capability (+ optional evidence already in hand) or a product fact. Deduped, deterministic order. + """ + by_signal: Dict[str, List[SignalMapping]] = {} + for m in signal_map: + by_signal.setdefault(m.signal, []).append(m) + + caps: Dict[str, DetectedCapability] = {} + facts: Dict[str, ProductFact] = {} + evidence: Set[str] = set() + requirements: Set[str] = set() + for s in signals: + if s.kind != "observation": # a requirement describes a TARGET, never the present state + requirements.add(s.signal) # preserved + visible, but NEVER turned into a capability + continue + for m in by_signal.get(s.signal, []): + if m.capability and m.capability not in caps: + caps[m.capability] = DetectedCapability( + capability=m.capability, relationship=m.relationship, + source="%s:%s" % (s.source, s.signal), evidence=m.evidence, + confidence=s.confidence, provenance=s.provenance) + if m.evidence: + evidence.add(m.evidence) + if m.product_fact: + facts[m.product_fact] = ProductFact(key=m.product_fact, value=m.fact_value, source=s.source) + + detected = [caps[k] for k in sorted(caps)] + product_facts = [facts[k] for k in sorted(facts)] + requirements_seen = sorted(requirements) + n_detected = sum(1 for d in detected if d.relationship == "detected") # concrete artifacts -> auto-detected + n_indication = len(detected) - n_detected # partial -> indication, still asked + summary = ( + "Stille Vorbefüllung: %d Fähigkeit(en) automatisch erkannt, %d Indikation(en), %d Produktfakt(en), " + "%d Nachweis(e) bereits vorhanden, %d Anforderung(en) erkannt (nicht als vorhanden gewertet)." + % (n_detected, n_indication, len(product_facts), len(evidence), len(requirements_seen)) + ) + return SilentIntakeResult( + detected_capabilities=detected, product_facts=product_facts, + evidence_found=sorted(evidence), requirements_seen=requirements_seen, summary=summary) diff --git a/backend-compliance/compliance/optimization/__init__.py b/backend-compliance/compliance/optimization/__init__.py new file mode 100644 index 00000000..1d88319d --- /dev/null +++ b/backend-compliance/compliance/optimization/__init__.py @@ -0,0 +1,21 @@ +"""Regulatory Optimization — the Roadmap / Management renderer of the Capability Delta Engine. + +Ranks the OPEN Capability Delta (from RS-005) by regulatory leverage: which measure closes the +most regulatory requirements at once. Answers the Geschäftsführer question "Womit anfangen?". +Pure, deterministic, computed-not-stored. Consumes the RS-005 delta (acyclic dependency); the +delta engine stays hermetic. No new corpus, no new meta-model class (freeze v1.0). +""" + +from __future__ import annotations + +from .engine import regulatory_leverage, roadmap_from_delta, select_within_budget +from .schemas import BudgetPlan, OptimizationPlan, RankedMeasure + +__all__ = [ + "regulatory_leverage", + "select_within_budget", + "roadmap_from_delta", + "OptimizationPlan", + "RankedMeasure", + "BudgetPlan", +] diff --git a/backend-compliance/compliance/optimization/engine.py b/backend-compliance/compliance/optimization/engine.py new file mode 100644 index 00000000..21104006 --- /dev/null +++ b/backend-compliance/compliance/optimization/engine.py @@ -0,0 +1,134 @@ +"""Regulatory Optimization — the Roadmap / Management RENDERER of the Capability Delta Engine. + +GAP analysis and measure-prioritisation are TWO VIEWS OF THE SAME COMPUTATION. The Capability +Delta Engine (`compliance/transition_reasoning`, RS-005) computes Required - Known = the +Capability Delta once. Renderers read that ONE delta: + - Interview Renderer (missing INFORMATION -> questions) = `TransitionQuestionRequest` (built) + - Roadmap / Management Renderer (missing CAPABILITIES -> measures by leverage) = THIS module + - Evidence Renderer (missing EVIDENCE -> upload requests) = later +There is one truth, not a Gap engine and a separate Roadmap engine. + +A measure (a capability to implement) has *regulatory leverage* = the number of distinct +regulatory requirements it closes AT ONCE (e.g. patch management closes a CRA, a MaschinenVO, +an IEC 62443 and an ISO 27001 requirement -> leverage 4). The product turns from "you have N +obligations" into "of N identified requirements you only need M measures — and these K first". + +Fully deterministic, computed-not-stored, NO new corpus. `regulatory_leverage`/`select_within_budget` +are pure math over `capability -> requirements`; `roadmap_from_delta` binds them to the RS-005 +delta (dependency optimization -> transition_reasoning, acyclic; the delta engine stays hermetic). +No new graph/meta-model class (freeze v1.0). Python 3.9 compatible. + +Honesty (Welt-1): the percentages are exact count ratios over the IDENTIFIED requirements from +the known patterns — never "% gesetzeskonform". Label outputs as "der identifizierten Anforderungen". +""" + +from __future__ import annotations + +from typing import Dict, List, Optional + +from ..transition_reasoning import CoverageStatus, TransitionAssessment +from .schemas import BudgetPlan, OptimizationPlan, RankedMeasure + + +def _ranked( + capability_requirements: Dict[str, List[str]], in_scope: Optional[List[str]] +) -> List[RankedMeasure]: + """Rank measures: leverage desc, then capability_id asc (deterministic). Empty covers dropped.""" + scope = ( + set(in_scope) + if in_scope is not None + else {r for reqs in capability_requirements.values() for r in reqs} + ) + measures: List[RankedMeasure] = [] + for cap, reqs in capability_requirements.items(): + covers = sorted({r for r in reqs if r in scope}) + if not covers: + continue # this capability closes nothing in scope -> not a measure here + measures.append(RankedMeasure(capability_id=cap, covers=covers, leverage=len(covers))) + measures.sort(key=lambda m: (-m.leverage, m.capability_id)) + total = sum(m.leverage for m in measures) + running = 0 + for m in measures: + running += m.leverage + m.cumulative_requirements = running + m.cumulative_coverage = (running / total) if total else 0.0 + return measures + + +def regulatory_leverage( + capability_requirements: Dict[str, List[str]], in_scope: Optional[List[str]] = None +) -> OptimizationPlan: + """Rank measures by regulatory leverage; report the compression (requirements -> measures). + + `capability_requirements`: measure (capability_id) -> the requirement keys it satisfies. A + requirement key is currently a regulation (via `covers_targets`); finer obligation granularity + is a future extension. `in_scope`: restrict the requirement keys counted (default: all seen). + """ + measures = _ranked(capability_requirements, in_scope) + scope = sorted( + set(in_scope) + if in_scope is not None + else {r for reqs in capability_requirements.values() for r in reqs} + ) + total = sum(m.leverage for m in measures) + avg = (total / len(measures)) if measures else 0.0 + headline = ( + "%d identifizierte Anforderungen aus %d Regelwerken -> %d Massnahmen (Ø Hebel %.1f)." + % (total, len(scope), len(measures), avg) + ) + return OptimizationPlan( + in_scope_requirements=scope, + total_measures=len(measures), + total_requirements=total, + ranked_measures=measures, + headline=headline, + ) + + +def select_within_budget( + capability_requirements: Dict[str, List[str]], + budget: int, + in_scope: Optional[List[str]] = None, +) -> BudgetPlan: + """The budget answer: with K measures, pick the K highest-leverage ones and report coverage. + + Because each requirement key is closed by exactly one measure here, greedy-by-leverage is the + optimal cover, so ranking == selection. (When requirements become shared across capabilities, + this becomes weighted set-cover; the signature is ready for that.) + """ + measures = _ranked(capability_requirements, in_scope) + total = sum(m.leverage for m in measures) + k = max(0, budget) + selected = measures[:k] + closed = selected[-1].cumulative_requirements if selected else 0 + ratio = (closed / total) if total else 0.0 + headline = ( + "Mit den Top-%d Massnahmen (nach regulatorischem Hebel) schliessen Sie %d von %d " + "identifizierten Anforderungen (%.0f%%)." % (len(selected), closed, total, ratio * 100) + ) + return BudgetPlan( + budget=budget, + selected_capabilities=[m.capability_id for m in selected], + requirements_closed=closed, + total_requirements=total, + coverage_ratio=ratio, + headline=headline, + ) + + +def roadmap_from_delta( + assessment: TransitionAssessment, + capability_requirements: Dict[str, List[str]], + in_scope: Optional[List[str]] = None, + open_statuses: Optional[List[CoverageStatus]] = None, +) -> OptimizationPlan: + """Render the Roadmap view FROM a Capability Delta (an RS-005 `TransitionAssessment`). + + Takes the OPEN capabilities of the delta — MISSING by default — and ranks them by regulatory + leverage. This is the same delta the Interview Renderer turns into questions; here it becomes + prioritised measures. The binding that makes "one truth, two renderers" real in code. + """ + statuses = set(open_statuses) if open_statuses is not None else {CoverageStatus.MISSING} + open_caps = [c.capability_id for c in assessment.coverage if c.status in statuses] + delta_reqs = {cap: capability_requirements.get(cap, []) for cap in open_caps} + return regulatory_leverage(delta_reqs, in_scope) diff --git a/backend-compliance/compliance/optimization/schemas.py b/backend-compliance/compliance/optimization/schemas.py new file mode 100644 index 00000000..7ff1ee60 --- /dev/null +++ b/backend-compliance/compliance/optimization/schemas.py @@ -0,0 +1,48 @@ +"""Schemas for the Regulatory Optimization Engine. + +These DTOs are *derived views* (computed-not-stored): nothing here is persisted; every value +is recomputed from the input each call. No new meta-model class, no graph (freeze v1.0). +Python 3.9 compatible (no `|` unions). +""" + +from __future__ import annotations + +from typing import List + +from pydantic import BaseModel, Field + + +class RankedMeasure(BaseModel): + """One measure (a capability to implement) ranked by its regulatory leverage.""" + + capability_id: str + covers: List[str] = Field(default_factory=list) # the in-scope requirements it satisfies + leverage: int = 0 # = len(covers): how many it closes at once + cumulative_requirements: int = 0 # running total of requirements closed (ranked order) + cumulative_coverage: float = 0.0 # cumulative_requirements / total_requirements (0..1) + + +class OptimizationPlan(BaseModel): + """Measures ranked by regulatory leverage — greatest regulatory effect first. + + `total_requirements` counts the IDENTIFIED requirements in scope (the known delta from the + patterns), NOT a company's total legal duties. The percentages are exact count ratios over + this identified set — never a compliance verdict (Welt-1 discipline). + """ + + in_scope_requirements: List[str] = Field(default_factory=list) # the distinct requirement keys counted + total_measures: int = 0 # number of distinct measures (delta capabilities) + total_requirements: int = 0 # Sum of leverage = identified requirements closable + ranked_measures: List[RankedMeasure] = Field(default_factory=list) + headline: str = "" # "N identifizierte Anforderungen -> M Massnahmen ..." + + +class BudgetPlan(BaseModel): + """The budget answer: with a budget of K measures, which K and how much do they close?""" + + budget: int = 0 + selected_capabilities: List[str] = Field(default_factory=list) + requirements_closed: int = 0 + total_requirements: int = 0 + coverage_ratio: float = 0.0 # requirements_closed / total_requirements (0..1) + headline: str = "" diff --git a/backend-compliance/compliance/playbook/__init__.py b/backend-compliance/compliance/playbook/__init__.py new file mode 100644 index 00000000..e5ff3670 --- /dev/null +++ b/backend-compliance/compliance/playbook/__init__.py @@ -0,0 +1,20 @@ +"""Implementation Playbook — the Berater renderer ("wie komme ich dort hin?"). + +For one capability it assembles the full implementation journey (why / closes which regulations / +tools / process / evidence / controls) from curated playbook knowledge + regulatory leverage + +injected Execution links. `playbooks_for_plan` chains the Optimization Roadmap into per-measure +playbooks. Pure, deterministic, computed-not-stored. No new corpus, no new meta-model class +(freeze v1.0). Curated content = expert draft, never normative. +""" + +from __future__ import annotations + +from .engine import build_playbook, playbooks_for_plan +from .schemas import Playbook, PlaybookStep + +__all__ = [ + "build_playbook", + "playbooks_for_plan", + "Playbook", + "PlaybookStep", +] diff --git a/backend-compliance/compliance/playbook/engine.py b/backend-compliance/compliance/playbook/engine.py new file mode 100644 index 00000000..5083a46c --- /dev/null +++ b/backend-compliance/compliance/playbook/engine.py @@ -0,0 +1,96 @@ +"""Implementation Playbook — the Berater renderer ("wie komme ich dort hin?"). + +After the Capability Delta Engine says WHAT is missing and the Optimization renderer says WHICH +measure first, the Playbook renderer says HOW to implement it. For one capability it assembles the +full journey from three sources: + - curated playbook KNOWLEDGE (why / tools / process steps / evidence / how others do it) — the + Reasoning Knowledge Acquisition layer under `knowledge/implementation_playbooks/`, + - the regulatory LEVERAGE (which regulations a delivered capability closes) — reused from the + Optimization renderer, + - injected Procedure/Control/Evidence links (Execution-owned; empty until linked). + +Pure, deterministic, computed-not-stored. Chains optimization -> playbook (acyclic). No new corpus, +no new meta-model class (freeze v1.0). Python 3.9 compatible. + +The curated content is an EXPERT DRAFT, never a normative requirement. When no playbook knowledge +exists for a capability yet, the renderer emits a `status: missing` stub — the honest signal that +the bottleneck is CONTENT (Knowledge Acquisition), not software. +""" + +from __future__ import annotations + +from typing import Any, Dict, List, Optional + +from ..optimization import OptimizationPlan +from .schemas import Playbook, PlaybookStep + +_MISSING_WHY = "(Playbook-Inhalt fehlt — Knowledge Acquisition offen.)" +_DRAFT_DISCLAIMER = ( + "Kuratiertes Experten-Wissen (Erstentwurf), KEINE normative Anforderung. Tools/Schritte sind " + "Empfehlungen, kein Pflichtkatalog; Controls werden aus der Execution-Schicht injiziert." +) + + +def _steps(raw: Any) -> List[PlaybookStep]: + steps: List[PlaybookStep] = [] + for i, s in enumerate(raw or [], 1): + steps.append(PlaybookStep(order=i, title=str(s.get("title", "")), detail=str(s.get("detail", "")))) + return steps + + +def build_playbook( + capability_id: str, + knowledge: Optional[Dict[str, Any]] = None, + closes_regulations: Optional[List[str]] = None, + control_links: Optional[List[str]] = None, +) -> Playbook: + """Assemble the implementation journey for ONE capability. + + `knowledge`: the curated playbook dict (None/empty -> a `missing` stub). `closes_regulations`: + the regulations a delivered capability closes (leverage, from `covers_targets`). `control_links`: + Execution-owned control refs, injected (default empty — no Execution data in Reasoning code). + """ + closes = sorted(set(closes_regulations or [])) + if not knowledge: + return Playbook( + capability_id=capability_id, title=capability_id, why=_MISSING_WHY, + closes_regulations=closes, leverage=len(closes), controls=list(control_links or []), + status="missing", disclaimer=_DRAFT_DISCLAIMER, + ) + return Playbook( + capability_id=capability_id, + title=str(knowledge.get("title", capability_id)), + why=str(knowledge.get("why", "")), + closes_regulations=closes, + leverage=len(closes), + tools=list(knowledge.get("tools", [])), + process_steps=_steps(knowledge.get("process_steps")), + expected_evidence=list(knowledge.get("expected_evidence", [])), + controls=list(control_links or []), + how_others_do_it=str(knowledge.get("how_others_do_it", "")), + status=str(knowledge.get("status", "draft")), + disclaimer=str(knowledge.get("disclaimer", _DRAFT_DISCLAIMER)), + ) + + +def playbooks_for_plan( + plan: OptimizationPlan, + knowledge_by_cap: Dict[str, Dict[str, Any]], + top_k: Optional[int] = None, + control_links_by_cap: Optional[Dict[str, List[str]]] = None, +) -> List[Playbook]: + """Render playbooks for the highest-leverage measures of an OptimizationPlan (Roadmap -> How). + + Walks the ranked measures (top_k, or all) and builds each capability's playbook, using the + measure's own `covers` as the regulations it closes. Measures without curated knowledge become + `missing` stubs — surfacing exactly where playbook content is still owed. + """ + links = control_links_by_cap or {} + measures = plan.ranked_measures if top_k is None else plan.ranked_measures[: max(0, top_k)] + return [ + build_playbook( + m.capability_id, knowledge_by_cap.get(m.capability_id), + closes_regulations=m.covers, control_links=links.get(m.capability_id), + ) + for m in measures + ] diff --git a/backend-compliance/compliance/playbook/schemas.py b/backend-compliance/compliance/playbook/schemas.py new file mode 100644 index 00000000..1060a5e1 --- /dev/null +++ b/backend-compliance/compliance/playbook/schemas.py @@ -0,0 +1,45 @@ +"""Schemas for the Implementation Playbook renderer. + +A Playbook is a *derived view* (computed-not-stored): it assembles, for one capability, the full +"wie komme ich dort hin?" journey from (a) curated playbook KNOWLEDGE, (b) the regulatory leverage +(which regulations a delivered capability closes), and (c) injected Procedure/Control/Evidence links +(Execution-owned). Nothing here is persisted. No new meta-model class, no graph (freeze v1.0). +Python 3.9 compatible (no `|` unions). +""" + +from __future__ import annotations + +from typing import List + +from pydantic import BaseModel, Field + + +class PlaybookStep(BaseModel): + """One step in the recommended way to stand up a capability.""" + + order: int + title: str + detail: str = "" + + +class Playbook(BaseModel): + """The complete implementation journey for ONE capability — the Berater view. + + Answers, in order: Warum? -> Welche Regelwerke schliesst das? -> Welche Tools? -> Welche + Prozesse? -> Welche Nachweise? -> Welche Controls? The curated parts (why/tools/steps/evidence/ + how-others) are an EXPERT DRAFT, not a normative requirement; controls are injected from + Execution (may be empty until linked). + """ + + capability_id: str + title: str = "" + why: str = "" # why this is required (regulatory rationale) + closes_regulations: List[str] = Field(default_factory=list) # leverage: regulations a delivered cap closes + leverage: int = 0 # = len(closes_regulations) + tools: List[str] = Field(default_factory=list) # typical tooling (curated knowledge) + process_steps: List[PlaybookStep] = Field(default_factory=list) # how to stand it up + expected_evidence: List[str] = Field(default_factory=list) # artifacts that prove it + controls: List[str] = Field(default_factory=list) # control refs (injected from Execution; may be empty) + how_others_do_it: str = "" # "wie machen das andere?" (curated) + status: str = "draft" # draft -> reviewed -> validated -> proven + disclaimer: str = "" # expert draft, not a normative requirement diff --git a/backend-compliance/compliance/services/onboarding_service.py b/backend-compliance/compliance/services/onboarding_service.py new file mode 100644 index 00000000..95526a56 --- /dev/null +++ b/backend-compliance/compliance/services/onboarding_service.py @@ -0,0 +1,89 @@ +"""Onboarding Advisor service — the app-caller that loads knowledge and runs the pure orchestration. + +This is the SERVICE layer that makes the Smart Onboarding Advisor runtime-usable: it loads the curated +knowledge (certification hypotheses, signal vocabulary + map, the target's required capabilities) once +and calls the already-built, pure orchestration (normalize_signals -> silent_intake -> advisor_start). +It adds NO new reasoning logic — it only exposes what exists. No DB, no persistence (by scope). +""" + +from __future__ import annotations + +import os +from typing import Any, Dict, Iterable, List, Sequence, Tuple + +import yaml + +from compliance.onboarding import ( + AdvisorResult, + CapabilityHypothesis, + OnboardingInput, + ProducedSignal, + SignalMapping, + SignalVocabularyEntry, + advisor_start, + normalize_signals, + resolve_for_certifications, + silent_intake, +) +from compliance.transition_reasoning import TargetRequirement + +_K = os.path.join(os.path.dirname(__file__), "..", "..", "knowledge") + + +def _load(*parts: str) -> Any: + return yaml.safe_load(open(os.path.join(_K, *parts), encoding="utf-8")) + + +_HYP_LIB = [CapabilityHypothesis(**h) for h in _load("certification_hypotheses", "hypotheses.yaml")["hypotheses"]] +_VOCAB = [SignalVocabularyEntry(**v) for v in _load("onboarding", "signal_vocabulary.yaml")["signals"]] +_SIGNAL_MAP = [SignalMapping(**m) for m in _load("onboarding", "intake_signal_map.yaml")["mappings"]] +_LABELS: Dict[str, str] = _load("onboarding", "capability_labels.yaml")["labels"] + + +def labels_for(capability_ids: Iterable[str]) -> Dict[str, str]: + """Human labels (DE) for the given capability ids — presentation only. Ids without a curated label + are omitted (the frontend falls back to a prettified id). Deduped, deterministic.""" + return {c: _LABELS[c] for c in dict.fromkeys(capability_ids) if c in _LABELS} + +# target id -> transition pattern that defines its required capabilities (curated registry) +_TARGET_PATTERNS = { + "CRA": "transition_pattern_iso27001_to_cra_maschinenvo_v1.yaml", + "TISAX": "transition_pattern_isms_to_tisax_v1.yaml", + "MDR": "transition_pattern_iso13485_to_medical_v1.yaml", + "Environmental": "transition_pattern_iso14001_to_environmental_v1.yaml", +} + + +def supported_targets() -> List[str]: + return sorted(_TARGET_PATTERNS) + + +def _target(target_id: str) -> Tuple[List[TargetRequirement], Dict[str, List[str]]]: + pat = _load("transition_patterns", _TARGET_PATTERNS[target_id]) + reqs = [TargetRequirement(capability_id=a["capability"], rationale=a.get("reviewable_claim", "")) for a in pat["likely_covered"]] + reqs += [TargetRequirement(capability_id=d["capability"], question_intent=d.get("needed_information", "verify_existence"), + rationale=d.get("why_asked", ""), expected_evidence=d.get("expected_evidence", [])) + for d in pat["delta_requirements"]] + covers = {d["capability"]: d.get("covers_targets", []) for d in pat["delta_requirements"]} + return reqs, covers + + +def run_advisor( + company: str, certifications: Sequence[str], target: str, + signals: Sequence[ProducedSignal], known_evidence: Sequence[str], + products: Sequence[str], markets: Sequence[str], industry: str = "", +) -> Tuple[AdvisorResult, str]: + """Producers (ProducedSignal) -> Normalizer -> Silent Pass -> Advisor. Returns an AdvisorResult. + + `target` must be a supported target id. Raises KeyError otherwise (the handler maps it to 400/404). + """ + reqs, covers = _target(target) + si = silent_intake(normalize_signals(signals, _VOCAB), _SIGNAL_MAP) + inp = OnboardingInput(company=company, industry=industry or None, products=list(products), + markets=list(markets), certifications=list(certifications), + known_evidence=list(known_evidence), target=[target]) + result = advisor_start( + inp, resolve_for_certifications(certifications, _HYP_LIB), reqs, target_id=target, + covers_targets=covers, corpus_status={target: "validated"}, + detected_capabilities=si.capability_ids(), indicative_capabilities=si.indicative_capability_ids()) + return result, si.summary diff --git a/backend-compliance/compliance/transition_reasoning/__init__.py b/backend-compliance/compliance/transition_reasoning/__init__.py new file mode 100644 index 00000000..bd133319 --- /dev/null +++ b/backend-compliance/compliance/transition_reasoning/__init__.py @@ -0,0 +1,45 @@ +"""Transition Reasoning v0 (RS-005) — the Transition Planning Engine. + +Answers „Was muss ich noch wissen, um vom Ausgangs- in den regulatorischen +Zielzustand zu kommen?". Owns the **information gaps** (`TransitionQuestionRequest`), +NOT the rendered questions (rendering = separate RS-005.1 layer). + +Consumes the Company Capability Profile (2A) as „have" + injected `TargetRequirement` +(Execution-owned placeholder) as „required". Spec: docs-src/architecture/transition-reasoning-spec-v1.md. +""" + +from __future__ import annotations + +from .convergence import RegulatoryConvergence, regulatory_convergence +from .engine import EMPTY_REQUIREMENTS, assess_transition +from .schemas import ( + CapabilityCoverage, + CoverageStatus, + InformationGain, + RequestPriority, + TargetRequirement, + TargetType, + TransitionAssessment, + TransitionContext, + TransitionGoal, + TransitionQuestionRequest, + TransitionSummary, +) + +__all__ = [ + "assess_transition", + "EMPTY_REQUIREMENTS", + "TransitionContext", + "TransitionGoal", + "TargetType", + "TargetRequirement", + "TransitionQuestionRequest", + "CapabilityCoverage", + "CoverageStatus", + "RequestPriority", + "InformationGain", + "TransitionSummary", + "TransitionAssessment", + "regulatory_convergence", + "RegulatoryConvergence", +] diff --git a/backend-compliance/compliance/transition_reasoning/convergence.py b/backend-compliance/compliance/transition_reasoning/convergence.py new file mode 100644 index 00000000..cb0229cd --- /dev/null +++ b/backend-compliance/compliance/transition_reasoning/convergence.py @@ -0,0 +1,54 @@ +"""Cross-Regulation Capability Mapping (Regulatory Convergence) — RS-005. + +Answers the USP question: „Welche Capability deckt gleichzeitig mehrere Regelwerke ab?" +Given, per capability, the set of target regulations it covers (`covers_targets`, +curated knowledge from a Regulatory Convergence Pattern), it computes how many +capabilities cover >= 2 regulations at once — the basis for the customer sentence +„von N Maßnahmen decken M gleichzeitig CRA und MaschinenVO". + +Pure, deterministic, computed-not-stored. No new graph/base class/meta-model class +(freeze v1.0 untouched). Python 3.9 compatible (no `|` unions). +""" + +from __future__ import annotations + +from typing import Dict, List + +from pydantic import BaseModel, Field + + +class RegulatoryConvergence(BaseModel): + targets: List[str] = Field(default_factory=list) + per_target_count: Dict[str, int] = Field(default_factory=dict) # capabilities each target requires + multi_target_capabilities: List[str] = Field(default_factory=list) # cover >= 2 targets + single_target_capabilities: List[str] = Field(default_factory=list) + headline: str = "" # NO percentages + + +def regulatory_convergence( + capability_targets: Dict[str, List[str]], targets: List[str] +) -> RegulatoryConvergence: + """capability_targets: capability_id -> regulations it covers. `targets`: the regulations in scope.""" + target_set = set(targets) + per: Dict[str, int] = {t: 0 for t in targets} + multi: List[str] = [] + single: List[str] = [] + for cap, covers in capability_targets.items(): + in_scope = [t for t in covers if t in target_set] + for t in in_scope: + per[t] += 1 + if len(in_scope) >= 2: + multi.append(cap) + elif len(in_scope) == 1: + single.append(cap) + headline = ( + "%d von %d Capabilities decken >= 2 Regelwerke gleichzeitig ab (%s)." + % (len(multi), len(capability_targets), " + ".join(targets)) + ) + return RegulatoryConvergence( + targets=list(targets), + per_target_count=per, + multi_target_capabilities=sorted(multi), + single_target_capabilities=sorted(single), + headline=headline, + ) diff --git a/backend-compliance/compliance/transition_reasoning/engine.py b/backend-compliance/compliance/transition_reasoning/engine.py new file mode 100644 index 00000000..7455a57d --- /dev/null +++ b/backend-compliance/compliance/transition_reasoning/engine.py @@ -0,0 +1,139 @@ +"""Transition Reasoning v0 (RS-005) — the Transition Planning Engine. + +`assess_transition(context, target_requirements, company_profile)`: computes, per +required capability, the coverage from the company's „have" state (Phase 2A), and +emits a ranked list of `TransitionQuestionRequest` (information gaps) — NOT questions. + +Deterministic; nothing is stored. A certification-derived „probably covered" is Welt 1 +(a hint), so it produces a confirmation request, never „erfüllt". Python 3.9 compatible. +""" + +from __future__ import annotations + +from typing import Dict, List, Optional + +from compliance.company import CompanyCapabilityProfile, VerificationStatus + +from .schemas import ( + CapabilityCoverage, + CoverageStatus, + InformationGain, + RequestPriority, + TargetRequirement, + TransitionAssessment, + TransitionContext, + TransitionQuestionRequest, + TransitionSummary, +) + +EMPTY_REQUIREMENTS: List[TargetRequirement] = [] + +_STATUS_RANK = { # strongest „have" signal wins when a capability appears twice + VerificationStatus.CONFIRMED: 3, + VerificationStatus.INFERRED: 2, + VerificationStatus.DECLARED: 1, + VerificationStatus.UNKNOWN: 0, +} + + +def _have(profile: CompanyCapabilityProfile) -> Dict[str, VerificationStatus]: + out: Dict[str, VerificationStatus] = {} + for oc in profile.confirmed_capabilities: + out[oc.capability_id] = VerificationStatus.CONFIRMED + for c in profile.candidate_capabilities: + cur = out.get(c.capability_id) + if cur is None or _STATUS_RANK[c.verification_status] > _STATUS_RANK[cur]: + out[c.capability_id] = c.verification_status + return out + + +def _classify(req: TargetRequirement, have: Dict[str, VerificationStatus]) -> CoverageStatus: + if req.unsupported: + return CoverageStatus.UNSUPPORTED + status = have.get(req.capability_id) + if status == VerificationStatus.CONFIRMED: + return CoverageStatus.ALREADY_COVERED + if status == VerificationStatus.INFERRED: + return CoverageStatus.PROBABLY_COVERED + if status == VerificationStatus.DECLARED: + return CoverageStatus.NEEDS_CONFIRMATION + return CoverageStatus.MISSING + + +# coverage -> (request?, reason, base priority) +_REQUESTABLE = { + CoverageStatus.PROBABLY_COVERED: ("Vermutlich vorhanden (aus Zertifizierung) — mit Nachweis bestätigen.", RequestPriority.MEDIUM), + CoverageStatus.NEEDS_CONFIRMATION: ("Selbst angegeben — Nachweis steht aus.", RequestPriority.MEDIUM), + CoverageStatus.MISSING: ("Keine Anhaltspunkte im Unternehmensprofil — klären.", RequestPriority.HIGH), +} + + +def _gain(coverage: CoverageStatus, n_obligations: int) -> InformationGain: + base = InformationGain.HIGH if coverage == CoverageStatus.MISSING else ( + InformationGain.MEDIUM if coverage == CoverageStatus.NEEDS_CONFIRMATION else InformationGain.LOW + ) + if n_obligations >= 2 and base != InformationGain.HIGH: # more dependent obligations -> bump + return InformationGain.HIGH if base == InformationGain.MEDIUM else InformationGain.MEDIUM + return base + + +_PRIO_ORDER = {RequestPriority.HIGH: 0, RequestPriority.MEDIUM: 1, RequestPriority.LOW: 2} +_GAIN_ORDER = {InformationGain.HIGH: 0, InformationGain.MEDIUM: 1, InformationGain.LOW: 2} + + +def assess_transition( + context: TransitionContext, + target_requirements: Optional[List[TargetRequirement]] = None, + company_profile: Optional[CompanyCapabilityProfile] = None, +) -> TransitionAssessment: + reqs = EMPTY_REQUIREMENTS if target_requirements is None else target_requirements + have = _have(company_profile) if company_profile is not None else {} + + coverage: List[CapabilityCoverage] = [] + requests: List[TransitionQuestionRequest] = [] + buckets: Dict[CoverageStatus, List[str]] = {s: [] for s in CoverageStatus} + + for req in reqs: + status = _classify(req, have) + coverage.append( + CapabilityCoverage( + capability_id=req.capability_id, + status=status, + have_status=have[req.capability_id].value if req.capability_id in have else None, + ) + ) + buckets[status].append(req.capability_id) + if status in _REQUESTABLE: + default_reason, prio = _REQUESTABLE[status] + reason = req.rationale or default_reason # curated human text wins over the generic fallback + requests.append( + TransitionQuestionRequest( + capability_id=req.capability_id, + control_id=req.source_control_id, + reason=reason, + question_intent=req.question_intent, + expected_evidence=req.expected_evidence, + priority=prio, + information_gain=_gain(status, len(req.supports_obligations)), + ) + ) + + requests.sort(key=lambda r: (_PRIO_ORDER[r.priority], _GAIN_ORDER[r.information_gain], r.capability_id)) + + summary = TransitionSummary( + headline=( + "%d zu klären, %d bereits abgedeckt, %d vermutlich vorhanden, %d fehlt, %d n/a, %d nicht im Korpus." + % (len(requests), len(buckets[CoverageStatus.ALREADY_COVERED]), + len(buckets[CoverageStatus.PROBABLY_COVERED]), len(buckets[CoverageStatus.MISSING]), + len(buckets[CoverageStatus.NOT_APPLICABLE]), len(buckets[CoverageStatus.UNSUPPORTED])) + ), + what_to_clarify=[r.capability_id for r in requests], + already_covered=buckets[CoverageStatus.ALREADY_COVERED], + probably_covered=buckets[CoverageStatus.PROBABLY_COVERED], + missing=buckets[CoverageStatus.MISSING], + not_applicable=buckets[CoverageStatus.NOT_APPLICABLE], + unsupported=buckets[CoverageStatus.UNSUPPORTED], + ) + return TransitionAssessment( + target_id=context.target.target_id, coverage=coverage, question_requests=requests, summary=summary + ) diff --git a/backend-compliance/compliance/transition_reasoning/schemas.py b/backend-compliance/compliance/transition_reasoning/schemas.py new file mode 100644 index 00000000..8a25f485 --- /dev/null +++ b/backend-compliance/compliance/transition_reasoning/schemas.py @@ -0,0 +1,111 @@ +"""Transition Reasoning v0 (RS-005) — domain objects. + +The **Transition Planning Engine**: it answers „Was muss ich noch wissen, um vom +Ausgangszustand in den regulatorischen Zielzustand zu kommen?" — NOT „wie frage ich +das?". It therefore owns the **information gaps** (`TransitionQuestionRequest`), never +the rendered question text. Rendering (intent + subject -> sentence) is a separate, +swappable layer (RS-005.1 Question Generator) and is NOT part of this engine. + +v0 consumes the Company Capability Profile (Phase 2A) as the „have" state and an +INJECTED list of `TargetRequirement` as the Execution-owned „required" side (no +required-capability data in product code — same discipline as 2A's EMPTY_MAPPING). + +Welt-1-Grenze: a probable coverage (from a certification) is a hint, never „erfüllt"; +it produces a confirmation request, not a verdict. Application/reasoning types, NOT +meta-model classes (freeze v1.0 untouched). Python 3.9 compatible (no `|` unions). +""" + +from __future__ import annotations + +from enum import Enum +from typing import List, Optional + +from pydantic import BaseModel, Field + + +class TargetType(str, Enum): + REGULATION = "regulation" + CERTIFICATION = "certification" + FRAMEWORK = "framework" + + +class CoverageStatus(str, Enum): + ALREADY_COVERED = "already_covered" # confirmed in the company profile + PROBABLY_COVERED = "probably_covered" # inferred (e.g. from a certification) + NEEDS_CONFIRMATION = "needs_confirmation" # only declared / weak signal + MISSING = "missing" # no signal + NOT_APPLICABLE = "not_applicable" + UNSUPPORTED = "unsupported" # domain not yet in the corpus (future_corpus_needed) + + +class RequestPriority(str, Enum): + HIGH = "high" + MEDIUM = "medium" + LOW = "low" + + +class InformationGain(str, Enum): + HIGH = "high" + MEDIUM = "medium" + LOW = "low" + + +class TransitionGoal(BaseModel): + target_id: str # e.g. "CRA", "TISAX" + target_type: TargetType = TargetType.REGULATION + label: str = "" + + +class TransitionContext(BaseModel): + company_id: str = "" + known_certifications: List[str] = Field(default_factory=list) + known_regulations: List[str] = Field(default_factory=list) + target: TransitionGoal + + +# ── INJECTED (Execution-owned): what the target requires ─────────────────── +class TargetRequirement(BaseModel): + """One required capability for the target. In v0 injected; later resolved from + `Obligation -> Control -> Required Capability` + `Control -> question_intent`.""" + + capability_id: str # MCAP-... + question_intent: str = "verify_existence" # passed through to the request, not rendered + rationale: str = "" # curated human text (e.g. why_asked / reviewable_claim) — surfaced as the request reason + expected_evidence: List[str] = Field(default_factory=list) + source_control_id: Optional[str] = None + supports_obligations: List[str] = Field(default_factory=list) + unsupported: bool = False # domain not yet in the corpus + + +# ── the OWNED output: an information gap, NOT a question ─────────────────── +class TransitionQuestionRequest(BaseModel): + capability_id: str + control_id: Optional[str] = None + reason: str + question_intent: str # verify_existence / determine_duration / ... (rendered later) + expected_evidence: List[str] = Field(default_factory=list) + priority: RequestPriority + information_gain: InformationGain + + +class CapabilityCoverage(BaseModel): + capability_id: str + status: CoverageStatus + have_status: Optional[str] = None # the 2A VerificationStatus that drove it + + +class TransitionSummary(BaseModel): + headline: str = "" # counts, NO percentage + what_to_clarify: List[str] = Field(default_factory=list) # capability_ids with a request + already_covered: List[str] = Field(default_factory=list) + probably_covered: List[str] = Field(default_factory=list) + missing: List[str] = Field(default_factory=list) + not_applicable: List[str] = Field(default_factory=list) + unsupported: List[str] = Field(default_factory=list) + + +class TransitionAssessment(BaseModel): + target_id: str + coverage: List[CapabilityCoverage] = Field(default_factory=list) + question_requests: List[TransitionQuestionRequest] = Field(default_factory=list) # ranked + summary: TransitionSummary diff --git a/backend-compliance/knowledge/architecture_stability/integration_ledger.yaml b/backend-compliance/knowledge/architecture_stability/integration_ledger.yaml new file mode 100644 index 00000000..21971c68 --- /dev/null +++ b/backend-compliance/knowledge/architecture_stability/integration_ledger.yaml @@ -0,0 +1,115 @@ +# Architecture Stability + Knowledge Velocity ledger — Phase Ω (Evidence of Generality). +# +# The question is no longer "can the architecture do this?" but "where does it fail under real domain +# knowledge?". Two KPIs almost nobody measures: +# - Architecture Stability : per integrated Requirement Source — new runtime classes? new pipeline? +# (target: 0 / 0) +# - Knowledge Velocity : can a DOMAIN EXPERT integrate a new source WITHOUT a software developer? +# (target: every source = data_only) +# +# HOW TO INTEGRATE A NEW SOURCE: add a ROW under `sources`. That is the whole point — a new domain is a +# DATA change here, never a code change. If you ever have to add a row under `pipeline_functions`, the +# stability claim broke and Phase Ω failed; record it honestly. + +# --- Integrated Requirement Sources: each is DATA (a pattern / a Required set), run by the shared pipeline --- +# new_capability_types = distinct NEW capability ids the source introduced. NOT an architecture break — +# a FRÜHINDIKATOR for capability-model granularity: if a domain ever needs ~80 new types with 0 runtime +# change, the capability model is probably cut too coarse or too fine. Watch the number, not just 0/0. +sources: + - source: "Cyber Resilience Act (CRA)" + domain: industrial_automation + target_type: regulation + integrated_as: transition_pattern_data + new_runtime_classes: 0 + new_pipeline: false + new_capability_types: 13 + integration_kind: data_only + family: cyber + exercised_by: "customer_mission_1/2/3, journey_matcher_demo" + - source: "Maschinenverordnung (MaschinenVO)" + domain: industrial_automation + target_type: regulation + integrated_as: transition_pattern_data + new_runtime_classes: 0 + new_pipeline: false + new_capability_types: 4 + integration_kind: data_only + family: cyber + exercised_by: "customer_mission_1/3, journey_matcher_demo" + - source: "TISAX" + domain: automotive + target_type: certification + integrated_as: transition_pattern_data + new_runtime_classes: 0 + new_pipeline: false + new_capability_types: 5 + integration_kind: data_only + family: cyber + exercised_by: "customer_mission_3/5, journey_matcher_demo" + - source: "Public Tender (öffentliche Ausschreibung)" + domain: cross_industry + target_type: contract + integrated_as: injected_required_set + new_runtime_classes: 0 + new_pipeline: false + new_capability_types: 3 + integration_kind: data_only + family: cyber + exercised_by: "customer_mission_3/4" + - source: "OEM Specification (Lastenheft)" + domain: automotive + target_type: contract + integrated_as: injected_required_set + new_runtime_classes: 0 + new_pipeline: false + new_capability_types: 4 + integration_kind: data_only + family: cyber + exercised_by: "customer_mission_4" + - source: "ISO 14001 -> Environmental/Material (REACH/RoHS/Batterie/Wasser/Energie/Abfall)" + domain: environmental + target_type: regulation + integrated_as: transition_pattern_data + new_runtime_classes: 0 + new_pipeline: false + new_capability_types: 16 + integration_kind: data_only + family: non_cyber # FIRST non-cyber domain — the real generality test + exercised_by: "customer_mission_5, environmental_stress_test" + - source: "Automotive ECU for OEM X (CRA / UNECE R155+R156 / IATF 16949 / TISAX / ASPICE / OEM spec)" + domain: automotive + target_type: multi_source # 7 OVERLAPPING sources spanning regulation + certification + process + contract + integrated_as: multi_source_required_set + new_runtime_classes: 0 + new_pipeline: false + new_capability_types: 14 # of 27 required caps, 13 reuse existing MCAPs (48% -> registry converging) + integration_kind: data_only + family: cyber # convergence test: same capability fed by many sources, model stayed stable + exercised_by: "automotive_convergence_stress_test" + - source: "ISO 13485 -> Medical device (MDR / IEC 62304 / ISO 14971 / IEC 81001-5-1)" + domain: medical + target_type: regulation + integrated_as: transition_pattern_data + new_runtime_classes: 0 + new_pipeline: false + new_capability_types: 7 # of 11 delta caps, 4 REUSE cyber MCAPs (IEC 81001-5-1 = CRA security coupling) + integration_kind: data_only + family: non_cyber # safety/clinical domain WITH a security coupling — the harder joint test + exercised_by: "medical_stress_test" + +# --- One-time, domain-AGNOSTIC pipeline functions (built once, now FROZEN per Phase Ω). --- +# Listed for honesty so the stability KPI cannot be gamed: these are NOT per-domain costs. The last +# one (journey_matcher) was the final architectural building block. +pipeline_functions: + - { fn: "transition_reasoning (RS-005)", maps: "Capability -> Delta", layer: transformation } + - { fn: "optimization", maps: "Delta -> Roadmap", layer: transformation } + - { fn: "journey_matcher (ADR-011)", maps: "Delta -> Journey", layer: transformation } + - { fn: "playbook", maps: "Capability -> Playbook", layer: production } + - { fn: "completeness", maps: "coverage audit", layer: production } + - { fn: "company (2A)", maps: "Evidence -> Capability", layer: descriptive } + +# --- The architecture has settled into three non-overlapping knowledge layers (a good sign). --- +knowledge_layers: + descriptive: ["Requirements", "Capabilities", "Evidence"] # what IS + transformation: ["Delta", "Journey", "Roadmap"] # how to MOVE + production: ["Playbooks", "Verification", "Reference Scenarios"] # how to DO + PROVE diff --git a/backend-compliance/knowledge/capability_families/families.yaml b/backend-compliance/knowledge/capability_families/families.yaml new file mode 100644 index 00000000..034cd3e5 --- /dev/null +++ b/backend-compliance/knowledge/capability_families/families.yaml @@ -0,0 +1,48 @@ +# Capability Families — the curated WHY behind convergence (Phase Ω: understand the core, not add domains). +# +# Medical proved the registry CONVERGES: completely different worlds (clinical/MDR/patient safety) still +# surface the same capabilities at the top. The question is no longer "which MCAPs?" but "WHY do they +# recur?". The answer is that ~60-70 MCAPs reduce to a small set of FAMILIES — and each family has a +# reason it is universal. That reason is the moat: not "MCAP-0017 exists" but "why MCAP-0017 must exist". +# +# This file is CURATED INSIGHT (the reason), not computed. Assignment of a capability to a family is by +# token match, FIRST family in this order wins — so cross-cutting CORE families are checked before +# DOMAIN-specific ones. Core vs Domain itself is NOT stored here; it is COMPUTED from the data (a cap is +# Core when it recurs across independent domains + source types). `kind` below is only the family's +# expected nature, used as a hint in the report. No new runtime, no new architecture. + +families: + # ── CORE families (expected to recur across domains) ────────────────────────────────────── + - {id: risk, label: "Risk", kind: core, tokens: [risk, hazard, threat], + reason: "Universeller Prozess — jede Regulierung verlangt eine Risikobeurteilung."} + - {id: update, label: "Update", kind: core, tokens: [update], + reason: "Softwareprodukt — jedes vernetzte Produkt muss sicher aktualisierbar sein."} + - {id: vulnerability, label: "Vulnerability", kind: core, tokens: [vulnerability, vuln], + reason: "Produktbetrieb — Schwächen über die Lebensdauer behandeln."} + - {id: identity_access, label: "Identity & Access", kind: core, tokens: [access, authentication, identification, credentials], + reason: "Wer/was darf — Identität und Zugriff."} + - {id: inventory, label: "Inventory & Composition", kind: core, tokens: [sbom, inventory, material, substance, substances, rxswin], + reason: "Lieferkette/Stoffstrom — was steckt im Produkt? (SBOM = Software, Stoffliste = Material)."} + - {id: supplier, label: "Supplier", kind: core, tokens: [supplier, suppliers], + reason: "Lieferantensteuerung — Verantwortung über die Kette."} + - {id: incident, label: "Incident & Reporting", kind: core, tokens: [incident, advisories, disclosure], + reason: "Vorfälle erkennen, behandeln und melden."} + - {id: monitoring, label: "Monitoring & Audit", kind: core, tokens: [monitoring, logging, surveillance, audits, audit], + reason: "Beobachtung — Betrieb und Umfeld überwachen."} + - {id: lifecycle, label: "Lifecycle & Development", kind: core, tokens: [lifecycle, development, engineer, versions], + reason: "Produkt-/Software-Lebenszyklus."} + - {id: evidence_docs, label: "Documentation & Evidence", kind: core, tokens: [document, documentation, declaration, conformity, technical], + reason: "Nachweisführung — Konformität dokumentieren."} + - {id: configuration, label: "Configuration & Asset", kind: core, tokens: [configuration, asset], + reason: "Konfiguration und Asset-Kontrolle."} + # ── DOMAIN families (expected to stay within one domain) ────────────────────────────────── + - {id: environmental, label: "Environmental/Material", kind: domain, tokens: [environmental, water, wastewater, energy, waste, emission, emissions, chemical, rohs, reach, battery, recycling, passport], + reason: "Umwelt-/Stoff-spezifisch."} + - {id: medical, label: "Medical", kind: domain, tokens: [clinical, medical, benefit, safety, classify, udi, device, capa], + reason: "Medizin-/Patientensicherheit-spezifisch."} + - {id: automotive, label: "Automotive", kind: domain, tokens: [aspice, csms, sums, cybersecurity, prototype, campaigns, ppap, apqp, functional], + reason: "Automotive-/Funktionssicherheit-spezifisch."} + - {id: machine_safety, label: "Machine Safety", kind: domain, tokens: [machine, mechanical, guards, operating, corruption], + reason: "Maschinensicherheit-spezifisch."} + - {id: process_qms, label: "Process & QMS", kind: domain, tokens: [release, change, capability, improvement, aspects, compliance, verify, design, ce] + ,reason: "QM-/Prozessdisziplin."} diff --git a/backend-compliance/knowledge/certification_hypotheses/hypotheses.yaml b/backend-compliance/knowledge/certification_hypotheses/hypotheses.yaml new file mode 100644 index 00000000..dc127c74 --- /dev/null +++ b/backend-compliance/knowledge/certification_hypotheses/hypotheses.yaml @@ -0,0 +1,79 @@ +# Certification Capability Hypotheses — CAPABILITY-CENTRIC, shared core first. +# +# Proprietary norms (ISO/TISAX/PCI…) are NOT ingested. Instead each hypothesis is its own knowledge +# object: "IF a company holds these certifications, we EXPECT this capability with some probability — +# verification required". NOT "ISO 27001 HAS X" (Welt-2) but "ISO 27001 SUGGESTS X" (Welt-1). +# +# THE TRICK (reuse, not redundancy): a capability is written ONCE with `supported_by: [certs]`. Most +# management-system capabilities (document control, incident, supplier, audit, risk, asset, access, +# training, monitoring) recur across many certs, so ~40-60 hypotheses cover everything instead of ~300. +# Multi-certification then merges AUTOMATICALLY (a company's inferred caps = every hypothesis whose +# supported_by intersects its certs). capability ids match the existing transition patterns. +# +# Confidence is NOT stored on the hypothesis — it is COMPUTED from a SEPARATE, reviewed observation +# stream (observations.py): each answer is a richer Observation (confirmed/partial/refuted/n.a./unknown +# + scope note), and a raw answer NEVER changes a hypothesis directly (review gate). Capabilities a cert +# does NOT suggest (SBOM, CVD, support period, signed updates) simply have NO hypothesis -> they always +# stay in the delta and get asked. AI first draft (~95%), expert review + customer calibration follow. +# No norm text reproduced. No real names. + +hypotheses: + # ── SHARED CORE — management-system capabilities that recur across certifications ─────────── + - {id: HYP-document_control, capability: document_and_change_control, relationship: supports, kind: shared, + supported_by: [ISO9001, ISO13485, ISO27001, TISAX, ASPICE, IATF16949], + verification_required: true, question_intent: verify_existence, expected_evidence: [document_control_procedure]} + # NOTE: ISO13485 deliberately NOT here — its CAPA / quality-safety incident handling is not security + # incident management; that mapping was too broad (would seed false hypotheses). A dedicated + # manage_quality_and_safety_incidents capability can be added later if a target actually needs it. + - {id: HYP-incident_management, capability: incident_management, relationship: supports, kind: shared, + supported_by: [ISO27001, TISAX, IEC62443], + verification_required: true, question_intent: verify_existence, expected_evidence: [incident_procedure]} + - {id: HYP-supplier_security, capability: supplier_security, relationship: supports, kind: shared, + supported_by: [ISO27001, TISAX, IEC62443], + verification_required: true, question_intent: verify_existence, expected_evidence: [supplier_security_records]} + - {id: HYP-supplier_evaluation, capability: supplier_evaluation, relationship: supports, kind: shared, + supported_by: [ISO9001, IATF16949, ISO13485], + verification_required: true, question_intent: verify_existence, expected_evidence: [supplier_evaluation_records]} + - {id: HYP-access_control, capability: access_control_and_authentication, relationship: supports, kind: shared, + supported_by: [ISO27001, TISAX, IEC62443], + verification_required: true, question_intent: verify_existence, expected_evidence: [access_control_policy]} + - {id: HYP-logging_monitoring, capability: security_logging_and_monitoring, relationship: supports, kind: shared, + supported_by: [ISO27001, TISAX, IEC62443], + verification_required: true, question_intent: verify_existence, expected_evidence: [logging_configuration]} + - {id: HYP-asset_config, capability: asset_and_configuration_management, relationship: supports, kind: shared, + supported_by: [ISO27001, TISAX, IEC62443], + verification_required: true, question_intent: verify_existence, expected_evidence: [asset_inventory]} + - {id: HYP-vuln_management, capability: technical_vulnerability_management, relationship: partially_supports, kind: shared, + supported_by: [ISO27001, TISAX, IEC62443], + verification_required: true, question_intent: confirm_product_scope, expected_evidence: [vulnerability_management_process]} + - {id: HYP-isms, capability: information_security_management, relationship: supports, kind: shared, + supported_by: [ISO27001, TISAX], + verification_required: true, question_intent: verify_existence, expected_evidence: [isms_scope]} + - {id: HYP-cryptography, capability: cryptography, relationship: supports, kind: shared, + supported_by: [ISO27001, TISAX, IEC62443], + verification_required: true, question_intent: verify_existence, expected_evidence: [crypto_policy]} + - {id: HYP-training, capability: security_awareness_training, relationship: supports, kind: shared, + supported_by: [ISO27001, TISAX], + verification_required: true, question_intent: verify_existence, expected_evidence: [training_records]} + - {id: HYP-prototype_protection, capability: protect_prototypes, relationship: supports, kind: shared, + supported_by: [TISAX], + verification_required: true, question_intent: verify_existence, expected_evidence: [prototype_protection_policy]} + - {id: HYP-release_approval, capability: release_and_approval_process, relationship: supports, kind: shared, + supported_by: [ISO9001, IATF16949, ISO13485], + verification_required: true, question_intent: verify_existence, expected_evidence: [release_procedure]} + - {id: HYP-ce_conformity, capability: ce_conformity_assessment_and_technical_documentation, relationship: partially_supports, kind: shared, + supported_by: [ISO9001, IATF16949], + verification_required: true, question_intent: request_evidence, expected_evidence: [technical_documentation]} + # ── CERT-SPECIFIC — capabilities a single domain's certificate suggests ───────────────────── + - {id: HYP-secure_dev, capability: secure_development_lifecycle, relationship: partially_supports, kind: specific, + supported_by: [IEC62443, ASPICE], + verification_required: true, question_intent: verify_existence, expected_evidence: [secure_development_policy]} + - {id: HYP-csms, capability: cybersecurity_management_system, relationship: supports, kind: specific, + supported_by: [IEC62443], + verification_required: true, question_intent: verify_existence, expected_evidence: [csms_records]} + - {id: HYP-environmental_docs, capability: environmental_management_documentation, relationship: supports, kind: specific, + supported_by: [ISO14001], + verification_required: true, question_intent: verify_existence, expected_evidence: [environmental_aspects_register]} + - {id: HYP-software_process, capability: assess_software_process_capability, relationship: supports, kind: specific, + supported_by: [ASPICE], + verification_required: true, question_intent: verify_existence, expected_evidence: [aspice_assessment]} diff --git a/backend-compliance/knowledge/domains/automotive/source_capabilities.yaml b/backend-compliance/knowledge/domains/automotive/source_capabilities.yaml new file mode 100644 index 00000000..120c3e78 --- /dev/null +++ b/backend-compliance/knowledge/domains/automotive/source_capabilities.yaml @@ -0,0 +1,58 @@ +# Automotive multi-source capability data — the CONVERGENCE stress test (Phase Ω, test #2). +# +# Not another domain to validate domain-agnosticism (Environmental already proved that). This tests a +# DIFFERENT property: can the SAME capability be fed by MANY different Requirement Sources at once +# without the model becoming unstable? Realistic setup: a supplier developing an ECU for OEM X, who +# already holds ISO 9001 + IATF 16949 + TISAX + ASPICE (L2/3) + CSMS + SUMS. +# +# Each source lists the capabilities it REQUIRES. Overlap is deliberate and realistic — that overlap is +# exactly where the economic value lives ("one capability replaces five evidence worlds"). Capabilities +# are VERBS / stable ids reused from the cyber + environmental patterns where they recur, so the +# existing-vs-new (registry convergence) measurement is honest. Data only — no runtime code. + +goal: "Develop a new ECU (Steuergerät) and supply OEM X." + +sources: + - id: CRA + type: regulation + requires: [secure_signed_update_distribution, technical_vulnerability_management, + coordinated_vulnerability_disclosure, sbom_creation, access_control_and_authentication, + incident_management, secure_development_lifecycle, product_cyber_risk_assessment] + - id: UNECE_R155_CSMS + type: regulation + requires: [secure_signed_update_distribution, technical_vulnerability_management, + product_cyber_risk_assessment, incident_management, access_control_and_authentication, + cybersecurity_management_system, threat_analysis_and_risk_assessment] + - id: UNECE_R156_SUMS + type: regulation + requires: [secure_signed_update_distribution, software_update_management_system, + identify_software_versions_rxswin, document_update_campaigns] + - id: IATF_16949 + type: certification + requires: [document_and_change_control, supplier_evaluation, release_and_approval_process, + approve_production_parts_ppap, plan_product_quality_apqp] + - id: TISAX + type: certification + requires: [information_security_management, access_control_and_authentication, incident_management, + technical_vulnerability_management, supplier_security, protect_prototypes] + - id: ASPICE + type: process + requires: [assess_software_process_capability, engineer_requirements_process, + verify_software_process, manage_configuration_process] + - id: OEM_X_Spec + type: contract + requires: [secure_signed_update_distribution, technical_vulnerability_management, + provide_functional_safety_evidence, protect_prototypes, supplier_security, + identify_software_versions_rxswin, provide_dedicated_security_contact] + +# The company's certificates/processes -> the capabilities they make probably-present (Welt-1). +# (Management/process discipline + the security baseline an automotive supplier with these certs has.) +company_profile_capabilities: + ISO9001: [document_and_change_control, supplier_evaluation, release_and_approval_process] + IATF16949: [approve_production_parts_ppap, plan_product_quality_apqp] + TISAX: [information_security_management, access_control_and_authentication, incident_management, + technical_vulnerability_management, supplier_security, protect_prototypes] + ASPICE: [assess_software_process_capability, engineer_requirements_process, + verify_software_process, manage_configuration_process] + CSMS: [cybersecurity_management_system, threat_analysis_and_risk_assessment, product_cyber_risk_assessment] + SUMS: [software_update_management_system, document_update_campaigns] diff --git a/backend-compliance/knowledge/implementation_playbooks/README.md b/backend-compliance/knowledge/implementation_playbooks/README.md new file mode 100644 index 00000000..336dd8a2 --- /dev/null +++ b/backend-compliance/knowledge/implementation_playbooks/README.md @@ -0,0 +1,51 @@ +# Implementation Playbooks — curated knowledge ("wie komme ich dort hin?") + +**Curated implementation KNOWLEDGE in machine-readable form — not an algorithm, not runtime code.** +After the Capability Delta Engine says WHAT is missing and the Optimization renderer says WHICH +measure first, a Playbook says HOW to implement one capability: why it is required, which tools and +process steps stand it up, which evidence proves it, which controls it maps to, and which regulatory +gaps it closes. The renderer is `compliance/playbook/`; this directory holds the content it renders. + +Nothing imports these at runtime — `compliance/playbook` reads them and assembles a `Playbook` +view. Adding or curating a playbook is therefore **non-runtime → no deploy** (ADR-001). + +## Playbook vs. regulatory domain (a deliberate distinction) + +- A **Playbook** is BreakPilot's OWN knowledge layer: `Capability → recommended approach → tools → + process → typical evidence → controls`. It does not introduce a new regulation. +- A **regulatory domain** (e.g. ISO 14001 → environmental law) is a new *regulatory* knowledge + domain (obligations, applicability), owned with Legal Knowledge / Execution. + +These scale independently. Once a domain lands, every new capability it needs can immediately be +embedded into the SAME playbook mechanism — which is why playbooks come first. + +## The bottleneck is CONTENT, not software + +The renderer is small and done. The value now grows with the **number and depth of playbooks**. +A capability with no playbook renders as a `status: missing` stub (the honest "content owed" signal). +This is Reasoning's Knowledge Acquisition responsibility (same as `../transition_patterns/`): +AI produces the first draft offline; BreakPilot reviews, versions and OWNS the library. + +## Maturity lifecycle + +`draft (L1) → reviewed (L2, internal) → validated (L3, domain expert) → proven (L4, field)`. +Curated content is an **EXPERT DRAFT, never a normative requirement**; tools/steps are recommended +practice, not a mandatory catalogue. Controls are **injected from the Execution layer** (may be +empty until linked) — no Execution data lives in the playbook content or in Reasoning product code. + +## Schema (per file) + +`id` · `capability_id` · `status` · `title` · `why` · `tools[]` · `process_steps[{title, detail}]` +· `expected_evidence[]` · `typical_controls[]` (indicative) · `how_others_do_it` · `disclaimer`. +`closes_regulations` / `leverage` are NOT stored here — the renderer supplies them from the +Optimization leverage (`covers_targets`), so one playbook serves every regulation it closes. + +## Catalogue + +| Playbook | Capability | status | +|---|---|---| +| `playbook_sbom_creation_v1.yaml` | sbom_creation | `draft` (L1) | +| `playbook_coordinated_vulnerability_disclosure_v1.yaml` | coordinated_vulnerability_disclosure (PSIRT) | `draft` (L1) | + +Next candidates (high-leverage CRA/MaschinenVO delta): `security_update_support_period` · +`secure_signed_update_distribution` · `product_cyber_risk_assessment`. diff --git a/backend-compliance/knowledge/implementation_playbooks/playbook_ce_conformity_assessment_and_technical_documentation_v1.yaml b/backend-compliance/knowledge/implementation_playbooks/playbook_ce_conformity_assessment_and_technical_documentation_v1.yaml new file mode 100644 index 00000000..86c9562c --- /dev/null +++ b/backend-compliance/knowledge/implementation_playbooks/playbook_ce_conformity_assessment_and_technical_documentation_v1.yaml @@ -0,0 +1,66 @@ +# Implementation Playbook — curated KNOWLEDGE (the "wie komme ich dort hin?" layer), not runtime code. +# Capability: ce_conformity_assessment_and_technical_documentation. Expert FIRST DRAFT (machinery-safety / +# CE-conformity domain, IACE session). NOT a normative requirement. Renderer = compliance/playbook. + +id: PB-ce_conformity_assessment_and_technical_documentation-v1 +capability_id: ce_conformity_assessment_and_technical_documentation +status: draft # draft -> reviewed -> validated -> proven +version: 1 + +title: "CE-Konformitätsbewertung und Technische Dokumentation für Maschinen" +canonical_action: "Maschinenkonformität nachweisen" # Verb+Objekt (capability-is-a-verb-Experiment) + +why: > + Vor dem Inverkehrbringen muss der Hersteller die Konformität der Maschine nachweisen: das passende + Konformitätsbewertungsverfahren durchführen (Maschinenverordnung Anhang XI), die Technische + Dokumentation zusammenstellen (Anhang IV), die EU-Konformitätserklärung ausstellen (Anhang V) und die + CE-Kennzeichnung anbringen. Dies ist der ABSCHLUSS-Schritt, der Risikobeurteilung, Schutzmaßnahmen und + Betriebsanleitung zu einem konformen, marktfähigen Produkt bündelt. Für die in Anhang I gelisteten + (Hochrisiko-)Maschinen kann die Einbindung einer notifizierten Stelle erforderlich sein. Bei vernetzten + Maschinen greift dies mit der CRA-Konformität (Cyber) ineinander — eine integrierte technische Akte. + +tools: + - "Maschinenverordnung (EU) 2023/1230 Anhang IV (Inhalt der Technischen Dokumentation)" + - "Anhang XI (Konformitätsbewertungsverfahren) + Anhang I (Liste der Hochrisiko-Maschinen)" + - "Anhang V (EU-Konformitätserklärung)" + - "Harmonisierte Normen (Fundstellen im Amtsblatt -> Vermutungswirkung)" + - "NANDO-Datenbank (Auswahl einer notifizierten Stelle, falls erforderlich)" + +process_steps: + - title: "Maschine klassifizieren" + detail: "Prüfen, ob die Maschine in Anhang I (Hochrisiko) fällt — das bestimmt, welche Verfahren nach Anhang XI zulässig sind." + - title: "Konformitätsverfahren wählen" + detail: "Nicht-Hochrisiko: interne Fertigungskontrolle (Anhang VIII). Hochrisiko/Anhang I: je nach Fall EU-Baumusterprüfung oder umfassende Qualitätssicherung unter Einbindung einer notifizierten Stelle." + - title: "Harmonisierte Normen anwenden" + detail: "Anwendbare harmonisierte Normen heranziehen und die Anwendung dokumentieren — das verschafft Vermutungswirkung für die abgedeckten Anforderungen." + - title: "Technische Dokumentation zusammenstellen (Anhang IV)" + detail: "Allgemeine Beschreibung, Konstruktions-/Fertigungsunterlagen, vollständige RISIKOBEURTEILUNG, Liste der angewandten Normen, Betriebsanleitung, Prüf-/Berechnungsberichte (z. B. PL-/SIL-Nachweis)." + - title: "EU-Konformitätserklärung ausstellen (Anhang V)" + detail: "Eindeutige Maschinenidentifikation, Verweis auf angewandte Rechtsakte (MaschinenVO; ggf. CRA, EMV, NSR) + Normen; rechtsverbindliche Unterschrift." + - title: "CE-Kennzeichnung anbringen" + detail: "Sichtbar/lesbar/dauerhaft an der Maschine; ggf. Kennnummer der notifizierten Stelle." + - title: "Aufbewahren" + detail: "Technische Dokumentation + Konformitätserklärung über die vorgeschriebene Frist (i. d. R. 10 Jahre) für die Marktüberwachung verfügbar halten." + +expected_evidence: + - "Vollständige Technische Dokumentation nach Anhang IV (inkl. Risikobeurteilung + Betriebsanleitung)" + - "EU-Konformitätserklärung nach Anhang V (datiert, unterschrieben)" + - "Nachweis der CE-Kennzeichnung an der Maschine" + - "Falls erforderlich: EU-Baumusterprüfbescheinigung / Bescheinigung der notifizierten Stelle" + +typical_controls: # INDIKATIV — echte Control-Zuordnung kommt aus der Execution-Schicht + - "conformity_assessment_procedure" + - "technical_file" + - "ce_marking_declaration" + +how_others_do_it: > + Verbreitete Praxis: für nicht in Anhang I gelistete Maschinen interne Fertigungskontrolle plus + konsequente Anwendung harmonisierter Normen (Vermutungswirkung); für Hochrisiko-Maschinen frühzeitige + Einbindung einer notifizierten Stelle. Die Technische Dokumentation wird als lebendes Dokument geführt, + das Risikobeurteilung, Betriebsanleitung und Prüfberichte bündelt; bei vernetzten Produkten wird die + CRA-Cyber-Konformität in dieselbe technische Akte integriert (eine Erklärung, mehrere Rechtsakte). + +disclaimer: > + Kuratiertes Experten-Wissen (Erstentwurf, Maschinensicherheit / CE-Konformität), KEINE normative + Anforderung. Werkzeug- und Schrittempfehlungen sind Beispiele bewährter Praxis, kein Pflichtkatalog. + Review durch eine:n CE-/Maschinensicherheits-Expert:in ausstehend (status: draft). diff --git a/backend-compliance/knowledge/implementation_playbooks/playbook_coordinated_vulnerability_disclosure_v1.yaml b/backend-compliance/knowledge/implementation_playbooks/playbook_coordinated_vulnerability_disclosure_v1.yaml new file mode 100644 index 00000000..21665302 --- /dev/null +++ b/backend-compliance/knowledge/implementation_playbooks/playbook_coordinated_vulnerability_disclosure_v1.yaml @@ -0,0 +1,58 @@ +# Implementation Playbook — curated KNOWLEDGE (the "wie komme ich dort hin?" layer), not runtime code. +# Capability: coordinated_vulnerability_disclosure (PSIRT). Expert FIRST DRAFT — a product-security +# practitioner would validate this; NOT a normative requirement. + +id: PB-coordinated_vulnerability_disclosure-v1 +capability_id: coordinated_vulnerability_disclosure +status: draft # draft -> reviewed -> validated -> proven +version: 1 + +title: "Coordinated Vulnerability Disclosure (CVD) / PSIRT aufbauen" + +why: > + Der CRA verlangt eine Richtlinie zur koordinierten Offenlegung von Schwachstellen und einen + Meldekanal (Annex I Teil II) sowie deren Bearbeitung. Forscher und Nutzer brauchen einen Weg, + Schwachstellen verantwortungsvoll zu melden; der Hersteller braucht einen Prozess, um zu + triagieren, zu beheben und abgestimmt zu veröffentlichen. Diese Funktion heißt PSIRT (Product + Security Incident Response Team). + +tools: + - "security.txt (RFC 9116) — maschinenlesbarer Sicherheitskontakt" + - "Veröffentlichte CVD-Policy (Webseite)" + - "Dediziertes PSIRT-Postfach + PGP-Schlüssel" + - "Triage-/Ticketsystem mit CVSS-Bewertung" + - "CSAF (Common Security Advisory Framework) für maschinenlesbare Advisories" + - "CVE Numbering Authority (CNA) — optional, für eigene CVE-Vergabe" + +process_steps: + - title: "CVD-Policy + Kontakt veröffentlichen" + detail: "Öffentliche Richtlinie (Scope, Safe-Harbor, Fristen) + security.txt mit Kontakt/PGP." + - title: "Meldekanal betreiben" + detail: "PSIRT-Postfach/Portal, verschlüsselte Übermittlung, Eingangsbestätigung mit SLA." + - title: "Triage + Schweregrad" + detail: "Eingang reproduzieren, CVSS bewerten, Betroffenheit über die SBOM bestimmen." + - title: "Behebung koordinieren" + detail: "Fix + Offenlegungsfrist mit dem Melder abstimmen (Coordinated Disclosure)." + - title: "CVE zuweisen" + detail: "Über die eigene oder eine partnernde CNA eine CVE-ID vergeben." + - title: "Advisory veröffentlichen + Nutzer benachrichtigen" + detail: "Maschinenlesbares CSAF-Advisory; betroffene Nutzer/Behörden informieren (verbindet mit Meldepflicht)." + +expected_evidence: + - "Veröffentlichte CVD-Policy + security.txt mit Kontakt" + - "PSIRT-Triage-Runbook (Rollen, Fristen, CVSS)" + - "Beispiel-Advisory (CSAF oder gleichwertig)" + +typical_controls: # INDIKATIV — echte Control-Zuordnung kommt aus der Execution-Schicht + - "vulnerability_disclosure" + - "incident_response" + +how_others_do_it: > + Verbreitete Praxis: security.txt + öffentliche CVD-Policy, ein PSIRT-Postfach mit PGP, Triage per + CVSS und Betroffenheitsanalyse über die SBOM, abgestimmte Veröffentlichung als CSAF-Advisory. + Viele Hersteller werden selbst CNA, um CVEs zeitnah vergeben zu können. + +disclaimer: > + Kuratiertes Experten-Wissen (Erstentwurf), KEINE normative Anforderung. Werkzeug- und + Schrittempfehlungen sind bewährte Praxis, kein Pflichtkatalog. Review durch eine:n PSIRT-/ + Product-Security-Expert:in ausstehend (status: draft). diff --git a/backend-compliance/knowledge/implementation_playbooks/playbook_machine_safety_risk_assessment_v1.yaml b/backend-compliance/knowledge/implementation_playbooks/playbook_machine_safety_risk_assessment_v1.yaml new file mode 100644 index 00000000..eb6e7d4c --- /dev/null +++ b/backend-compliance/knowledge/implementation_playbooks/playbook_machine_safety_risk_assessment_v1.yaml @@ -0,0 +1,64 @@ +# Implementation Playbook — curated KNOWLEDGE (the "wie komme ich dort hin?" layer), not runtime code. +# Capability: machine_safety_risk_assessment. Expert FIRST DRAFT (machinery-safety domain, IACE session) +# — a machine-safety engineer would validate this; it is NOT a normative requirement. The renderer +# (compliance/playbook) assembles it into the journey. + +id: PB-machine_safety_risk_assessment-v1 +capability_id: machine_safety_risk_assessment +status: draft # draft -> reviewed -> validated -> proven +version: 1 + +title: "Maschinen-Risikobeurteilung nach ISO 12100 durchführen" +canonical_action: "Maschinenrisiken systematisch beurteilen" # Verb+Objekt (capability-is-a-verb-Experiment) + +why: > + Die Maschinenverordnung (EU) 2023/1230 verlangt vor dem Inverkehrbringen eine Risikobeurteilung über + den GESAMTEN Lebenszyklus der Maschine (Transport, Montage, Inbetriebnahme, bestimmungsgemäßer Betrieb, + Reinigung, Wartung, Störungsbeseitigung, Demontage, Entsorgung). Sie ist die Grundlage, aus der sich + ALLE weiteren Schutzmaßnahmen und die Technische Dokumentation (Anhang IV) ableiten. ISO 12100 ist die + harmonisierte Methodik und verschafft Vermutungswirkung. Ohne dokumentierte Risikobeurteilung ist die + Auswahl von Schutzeinrichtungen, Not-Halt und Restrisiko-Hinweisen nicht begründbar. + +tools: + - "ISO 12100 (Risikobeurteilung + Risikominderung, Grundnorm)" + - "ISO/TR 14121-2 (praktische Beispiele für die Methodik)" + - "Gefährdungslisten ISO 12100 Anhang B (mechanisch, elektrisch, thermisch, Lärm, Vibration, Strahlung, Werkstoff, Ergonomie)" + - "Sistema (DGUV, PLr-/PL-Ermittlung für Schutzfunktionen)" + - "Risikograph-/Risikomatrix-Vorlagen (S x F x P bzw. S x E)" + +process_steps: + - title: "Grenzen der Maschine festlegen" + detail: "Verwendungs-, räumliche, zeitliche und Lebenszyklusgrenzen; bestimmungsgemäße Verwendung UND vernünftigerweise vorhersehbare Fehlanwendung." + - title: "Gefährdungen identifizieren" + detail: "Systematisch je Lebensphase und je Bereich anhand der ISO-12100-Anhang-B-Liste — nichts überspringen (gerade Wartung/Reinigung wird oft vergessen)." + - title: "Risiko einschätzen" + detail: "Je Gefährdung Schadensschwere x Eintrittswahrscheinlichkeit (Häufigkeit/Aufenthaltsdauer, Möglichkeit zur Vermeidung) bestimmen." + - title: "Risiko bewerten" + detail: "Entscheiden, ob Risikominderung erforderlich ist (akzeptables Restrisiko erreicht?)." + - title: "3-Stufen-Methode anwenden (in dieser Reihenfolge)" + detail: "1. inhärent sichere Konstruktion -> 2. technische Schutzmaßnahmen/ergänzende Schutzeinrichtungen -> 3. Benutzerinformation. Maßnahmen NICHT in Stufe 3 verlagern, was in Stufe 1/2 lösbar ist." + - title: "Iterieren" + detail: "Nach jeder Maßnahme Risiko neu bewerten; prüfen, ob neue Gefährdungen entstanden sind (z. B. eine Schutzhaube erzeugt eine Quetschstelle)." + - title: "Restrisiko dokumentieren" + detail: "Verbleibende Restrisiken festhalten und an die Betriebsanleitung übergeben (Schnittstelle zu operating_instructions_and_safety_information)." + +expected_evidence: + - "Risikobeurteilungsbericht über alle Lebensphasen (datiert, versioniert)" + - "Gefährdungsliste mit Risikoeinschätzung je Gefährdung" + - "Zuordnung Gefährdung -> gewählte Maßnahme -> verbleibendes Restrisiko" + - "Liste der Restrisiken (Eingang in die Betriebsanleitung)" + +typical_controls: # INDIKATIV — echte Control-Zuordnung kommt aus der Execution-Schicht + - "machine_risk_assessment_record" + - "three_step_risk_reduction" + +how_others_do_it: > + Verbreitete Praxis: Risikobeurteilung strikt nach ISO 12100 strukturieren, mit ISO/TR 14121-2 als + Beispiel-Steinbruch, und die ermittelten Schutzfunktionen über Sistema auf den erforderlichen + Performance Level (PLr) rechnen. Der Bericht wird unmittelbar Teil der Technischen Dokumentation + (Anhang IV) und wird bei jeder wesentlichen Veränderung der Maschine erneut gefahren. + +disclaimer: > + Kuratiertes Experten-Wissen (Erstentwurf, Maschinensicherheit), KEINE normative Anforderung. Werkzeug- + und Schrittempfehlungen sind Beispiele bewährter Praxis, kein Pflichtkatalog. Review durch eine:n + Maschinensicherheits-Expert:in ausstehend (status: draft). diff --git a/backend-compliance/knowledge/implementation_playbooks/playbook_mechanical_safety_and_guards_v1.yaml b/backend-compliance/knowledge/implementation_playbooks/playbook_mechanical_safety_and_guards_v1.yaml new file mode 100644 index 00000000..1df142af --- /dev/null +++ b/backend-compliance/knowledge/implementation_playbooks/playbook_mechanical_safety_and_guards_v1.yaml @@ -0,0 +1,69 @@ +# Implementation Playbook — curated KNOWLEDGE (the "wie komme ich dort hin?" layer), not runtime code. +# Capability: mechanical_safety_and_guards. Expert FIRST DRAFT (machinery-safety domain, IACE session). +# NOT a normative requirement. Renderer = compliance/playbook. + +id: PB-mechanical_safety_and_guards-v1 +capability_id: mechanical_safety_and_guards +status: draft # draft -> reviewed -> validated -> proven +version: 1 + +title: "Schutzeinrichtungen, Not-Halt und sichere Steuerung auslegen" +canonical_action: "Gefahrstellen wirksam absichern" # Verb+Objekt (capability-is-a-verb-Experiment) + +why: > + Die grundlegenden Sicherheits- und Gesundheitsschutzanforderungen der Maschinenverordnung (Anhang III) + verlangen Schutz vor mechanischen Gefährdungen (Quetschen, Scheren, Schneiden, Einziehen, Stoß), + funktionssichere Not-Halt-Einrichtungen, Schutz gegen unerwarteten Anlauf und Standsicherheit. Aus der + Risikobeurteilung (machine_safety_risk_assessment) ergibt sich, WO und mit WELCHER Zuverlässigkeit + (Performance Level / SIL) abgesichert werden muss. Trennende und nichttrennende Schutzeinrichtungen + setzen die zweite Stufe der 3-Stufen-Methode um. + +tools: + - "ISO 14120 (trennende Schutzeinrichtungen — Gestaltung)" + - "ISO 14119 (Verriegelungseinrichtungen mit/ohne Zuhaltung)" + - "ISO 13850 (Not-Halt-Funktion, Stopp-Kategorie 0/1)" + - "ISO 13857 (Sicherheitsabstände gegen Erreichen von Gefahrstellen)" + - "ISO 13849-1/-2 (sicherheitsbezogene Steuerungsteile, PL + Validierung)" + - "IEC 62061 (funktionale Sicherheit von Steuerungen, SIL)" + - "ISO 14118 (Vermeidung von unerwartetem Anlauf / LOTO)" + +process_steps: + - title: "Gefahrstellen übernehmen" + detail: "Aus der Risikobeurteilung die mechanischen Gefahrstellen + erforderlichen PLr/SIL je Schutzfunktion übernehmen." + - title: "Schutzeinrichtung wählen" + detail: "Feststehend trennend, wo im Betrieb kein Zugang nötig ist; beweglich trennend, wo regelmäßiger Zugang erforderlich ist; ergänzend nichttrennend (z. B. Lichtgitter), wo Materialfluss offen bleibt." + - title: "Bewegliche Schutzeinrichtungen verriegeln" + detail: "Nach ISO 14119 Verriegelung (mit Zuhaltung, wenn Nachlauf gefährlich ist); manipulationsarme Auslegung beachten." + - title: "Sicherheitsabstände einhalten" + detail: "Nach ISO 13857 Reichweiten nach oben/über/um Schutzeinrichtungen sicherstellen." + - title: "Not-Halt vorsehen" + detail: "ISO 13850: gut erreichbare Not-Halt-Befehlsgeräte, Stopp-Kategorie 0 oder 1, Rückstellung nur bewusst; Not-Halt ist ERGÄNZEND, ersetzt keine trennende Schutzeinrichtung." + - title: "Sichere Steuerung auslegen" + detail: "Sicherheitsbezogene Steuerungsteile auf den erforderlichen PL (ISO 13849-1) bzw. SIL (IEC 62061) bringen; Architektur, MTTFd, DC, CCF nachweisen (Sistema)." + - title: "Unerwarteten Anlauf verhindern" + detail: "ISO 14118 / sichere Energietrennung (LOTO) für Wartung und Störungsbeseitigung (Schnittstelle zur Wartungs-Lebensphase)." + - title: "Validieren" + detail: "Schutzfunktionen nach ISO 13849-2 verifizieren/validieren (Funktionstest + Nachweis)." + +expected_evidence: + - "Schutzkonzept (welche Gefahrstelle -> welche Schutzeinrichtung)" + - "PL-/SIL-Nachweis je Schutzfunktion (z. B. Sistema-Report)" + - "Funktionstest-Protokoll Not-Halt + Verriegelungen" + - "Validierungsbericht der Schutzeinrichtungen (ISO 13849-2)" + +typical_controls: # INDIKATIV — echte Control-Zuordnung kommt aus der Execution-Schicht + - "guard_design" + - "interlock_function" + - "emergency_stop_function" + - "safe_control_system" + +how_others_do_it: > + Verbreitete Praxis: feststehende Verkleidungen überall dort, wo kein betrieblicher Zugang nötig ist; + verriegelte bewegliche Schutztüren mit Zuhaltung an Stellen mit Nachlauf; der erforderliche Performance + Level wird per Risikograph bestimmt und mit Sistema nachgewiesen. Not-Halt wird als ergänzende, nicht + als primäre Maßnahme eingeplant. Validierung erfolgt dokumentiert vor der Konformitätserklärung. + +disclaimer: > + Kuratiertes Experten-Wissen (Erstentwurf, Maschinensicherheit), KEINE normative Anforderung. + Werkzeug- und Schrittempfehlungen sind Beispiele bewährter Praxis, kein Pflichtkatalog. Review durch + eine:n Maschinensicherheits-Expert:in ausstehend (status: draft). diff --git a/backend-compliance/knowledge/implementation_playbooks/playbook_operating_instructions_and_safety_information_v1.yaml b/backend-compliance/knowledge/implementation_playbooks/playbook_operating_instructions_and_safety_information_v1.yaml new file mode 100644 index 00000000..f823c566 --- /dev/null +++ b/backend-compliance/knowledge/implementation_playbooks/playbook_operating_instructions_and_safety_information_v1.yaml @@ -0,0 +1,63 @@ +# Implementation Playbook — curated KNOWLEDGE (the "wie komme ich dort hin?" layer), not runtime code. +# Capability: operating_instructions_and_safety_information. Expert FIRST DRAFT (machinery-safety domain, +# IACE session). NOT a normative requirement. Renderer = compliance/playbook. + +id: PB-operating_instructions_and_safety_information-v1 +capability_id: operating_instructions_and_safety_information +status: draft # draft -> reviewed -> validated -> proven +version: 1 + +title: "Betriebsanleitung und Sicherheitsinformationen erstellen" +canonical_action: "Sicherheitsinformationen bereitstellen" # Verb+Objekt (capability-is-a-verb-Experiment) + +why: > + Information für die Benutzung ist die DRITTE Stufe der 3-Stufen-Methode (ISO 12100 6.4): Sie trägt das + Restrisiko, das durch Konstruktion und technische Schutzmaßnahmen nicht beseitigt werden konnte. Die + Maschinenverordnung (Anhang III 1.7) verlangt eine Betriebsanleitung in der/den Sprache(n) des + Verwenderlandes; die Verordnung erlaubt nun unter Bedingungen auch die digitale Bereitstellung. Eine + vollständige, verständliche Anleitung ist Voraussetzung für die Konformität — fehlende oder + unverständliche Sicherheitsinformationen sind ein häufiger Mangel bei der Marktüberwachung. + +tools: + - "ISO 12100 6.4 (Information für die Benutzung)" + - "ISO 20607 (Betriebsanleitung — allgemeine Gestaltungsgrundsätze für Maschinensicherheit)" + - "IEC/IEEE 82079-1 (Erstellung von Nutzungsinformationen)" + - "ANSI Z535.6 / ISO 3864 (Sicherheits- und Warnhinweise, Signalwörter)" + - "Structured-Authoring-Werkzeuge (z. B. DITA) für Mehrsprachigkeit/Versionierung" + +process_steps: + - title: "Restrisiken übernehmen" + detail: "Die Restrisikoliste aus der Risikobeurteilung (machine_safety_risk_assessment) ist der verbindliche Eingang — jedes Restrisiko braucht eine Information/Warnung." + - title: "Zielgruppen und Lebensphasen abdecken" + detail: "Transport, Montage, Inbetriebnahme, Bedienung, Reinigung, Wartung, Störungsbeseitigung, Demontage/Entsorgung — je mit benötigter Qualifikation." + - title: "Warnhinweise strukturieren" + detail: "Einheitlich Signalwort (Gefahr/Warnung/Vorsicht) + Art der Gefahr + Folge + Vermeidung (ANSI Z535/ISO 3864)." + - title: "Verwendung abgrenzen" + detail: "Bestimmungsgemäße Verwendung UND vernünftigerweise vorhersehbare Fehlanwendung beschreiben." + - title: "Sprache sicherstellen" + detail: "In der/den Amtssprache(n) des Verwenderlandes; Übersetzungen als Übersetzung kennzeichnen." + - title: "Format und Bereitstellung festlegen" + detail: "Papier oder — nach den Bedingungen der Maschinenverordnung — digital; Sicherheitsinformationen müssen auffindbar und über die Lebensdauer verfügbar bleiben; auf Verlangen Papierfassung." + - title: "Versionieren" + detail: "Anleitung an Maschinenversion koppeln; bei wesentlicher Veränderung aktualisieren." + +expected_evidence: + - "Betriebsanleitung je Maschine und Sprache (datiert, versioniert)" + - "Warnhinweis-Konzept (Signalwörter, Piktogramme)" + - "Matrix Restrisiko -> zugehörige Information/Warnung (Rückverfolgbarkeit zur Risikobeurteilung)" + - "Nachweis der Bereitstellungsform (digital + Papier-auf-Verlangen)" + +typical_controls: # INDIKATIV — echte Control-Zuordnung kommt aus der Execution-Schicht + - "safety_information_provision" + - "residual_risk_communication" + +how_others_do_it: > + Verbreitete Praxis: strukturierte Redaktion nach IEC/IEEE 82079-1 und ISO 20607, Warnhinweise nach + ANSI Z535/ISO 3864, und eine durchgängige Verknüpfung jedes Restrisikos aus der Risikobeurteilung mit + einer konkreten Information. Digitale Anleitungen werden über stabile URLs/QR bereitgestellt, mit + Papierfassung auf Verlangen, und mit der ausgelieferten Maschinenversion verknüpft. + +disclaimer: > + Kuratiertes Experten-Wissen (Erstentwurf, Maschinensicherheit), KEINE normative Anforderung. + Werkzeug- und Schrittempfehlungen sind Beispiele bewährter Praxis, kein Pflichtkatalog. Review durch + eine:n Technische-Redaktion-/Maschinensicherheits-Expert:in ausstehend (status: draft). diff --git a/backend-compliance/knowledge/implementation_playbooks/playbook_protection_against_corruption_of_safety_functions_v1.yaml b/backend-compliance/knowledge/implementation_playbooks/playbook_protection_against_corruption_of_safety_functions_v1.yaml new file mode 100644 index 00000000..4f914a0d --- /dev/null +++ b/backend-compliance/knowledge/implementation_playbooks/playbook_protection_against_corruption_of_safety_functions_v1.yaml @@ -0,0 +1,71 @@ +# Implementation Playbook — curated KNOWLEDGE (the "wie komme ich dort hin?" layer), not runtime code. +# Capability: protection_against_corruption_of_safety_functions. Expert FIRST DRAFT (machinery-safety + +# cyber-safety bridge, IACE session). NOT a normative requirement. Renderer = compliance/playbook. +# +# CONVERGENCE NOTE: this capability is the CRA <-> MaschinenVO bridge. The same integrity + access +# controls that satisfy the CRA (software integrity, signed updates, access control) also satisfy +# Machinery Regulation Annex III 1.1.9. The renderer supplies closes_regulations/leverage from +# covers_targets — one playbook, two regulations. + +id: PB-protection_against_corruption_of_safety_functions-v1 +capability_id: protection_against_corruption_of_safety_functions +status: draft # draft -> reviewed -> validated -> proven +version: 1 + +title: "Sicherheitsfunktionen gegen (Software-)Korruption und Manipulation schützen" +canonical_action: "Sicherheitsfunktionen gegen Manipulation schützen" # Verb+Objekt (capability-is-a-verb) + +why: > + Maschinenverordnung Anhang III 1.1.9 verlangt, dass Hard- und Software sowie Daten, die für die + Sicherheit kritisch sind, gegen ZUFÄLLIGE und BEABSICHTIGTE Korruption geschützt werden. Eine + manipulierte oder fehlerhaft veränderte Sicherheitsfunktion (z. B. überbrückte Verriegelung, + verstellter Sicherheitsparameter, untergeschobenes Steuerungs-Update) wird unmittelbar zu einer + physischen Sicherheitsgefährdung. Genau hier treffen sich Maschinensicherheit und Cybersecurity: + dieselben Integritäts- und Zugriffsmaßnahmen, die der Cyber Resilience Act fordert, erfüllen auch + diese Maschinenpflicht (Konvergenz / Cyber-Safety-Brücke). + +tools: + - "IEC 62443 (industrielle Kommunikationsnetze / IACS-Security)" + - "IEC 61508 / ISO 13849 / IEC 62061 (funktionale Sicherheit der Steuerung)" + - "Code- und Update-Signierung (Secure Boot, signierte Firmware)" + - "Hardware-Vertrauensanker (TPM/Secure Element) für Integritätsprüfung" + - "Rollen-/Zugriffskonzept für sicherheitsrelevante Parameter (keine Default-Passwörter)" + +process_steps: + - title: "Sicherheitskritische Elemente identifizieren" + detail: "Welche Soft-/Hardware und welche Parameter/Daten tragen eine Sicherheitsfunktion? (Eingang aus der Risikobeurteilung + sicherer Steuerung.)" + - title: "Integrität schützen" + detail: "Signierte Firmware/Updates, Prüfsummen, sichere Bootkette; Veränderung sicherheitsrelevanten Codes nur mit verifizierter Signatur." + - title: "Zugriff kontrollieren" + detail: "Authentisierung für das Verstellen von Sicherheitsparametern; Rollentrennung; keine ab Werk gesetzten Standardpasswörter (secure-by-default)." + - title: "Safety und Standardsteuerung trennen" + detail: "Sicherheitsbezogene Steuerungsteile von der allgemeinen Steuerung entkoppeln, damit eine Kompromittierung der Standardseite die Safety-Funktion nicht aushebelt." + - title: "Manipulation erkennen -> sicherer Zustand" + detail: "Bei erkannter Integritätsverletzung definiert in den sicheren Zustand übergehen (fail-safe), nicht still weiterlaufen." + - title: "Updates sicher verteilen" + detail: "Sicherheitsrelevante Updates nur signiert und verifiziert einspielen (Schnittstelle zu sicheren Update-Capabilities des CRA)." + - title: "Sicherheitsrelevante Eingriffe protokollieren" + detail: "Änderungen an Sicherheitsparametern/-software nachvollziehbar loggen (Audit-Trail)." + +expected_evidence: + - "Konzept Integritätsschutz sicherheitskritischer Soft-/Hardware" + - "Nachweis Code-/Update-Signierung (Schlüsselverwaltung, Verifikation)" + - "Zugriffskontroll-/Rollenkonzept für Sicherheitsparameter" + - "Testprotokoll: Manipulationsversuch -> definierter sicherer Zustand" + +typical_controls: # INDIKATIV — echte Control-Zuordnung kommt aus der Execution-Schicht + - "safety_function_integrity" + - "secure_update_distribution" + - "access_control_safety_parameters" + +how_others_do_it: > + Verbreitete Praxis: die CRA-Maßnahmen (signierte Updates, Secure Boot, Integritätsprüfung, + Zugriffskontrolle ohne Default-Passwörter) werden EINMAL umgesetzt und decken zugleich + Maschinenverordnung Anhang III 1.1.9 ab — eine Maßnahme, zwei Regelwerke. Die Sicherheitssteuerung + wird nach IEC 62443 segmentiert, und bei Integritätsverletzung geht die Maschine kontrolliert in den + sicheren Zustand statt weiterzulaufen. + +disclaimer: > + Kuratiertes Experten-Wissen (Erstentwurf, Maschinensicherheit + Cyber-Safety), KEINE normative + Anforderung. Werkzeug- und Schrittempfehlungen sind Beispiele bewährter Praxis, kein Pflichtkatalog. + Review durch Product-Security- + Funktionale-Sicherheit-Expert:innen ausstehend (status: draft). diff --git a/backend-compliance/knowledge/implementation_playbooks/playbook_sbom_creation_v1.yaml b/backend-compliance/knowledge/implementation_playbooks/playbook_sbom_creation_v1.yaml new file mode 100644 index 00000000..dc8145ec --- /dev/null +++ b/backend-compliance/knowledge/implementation_playbooks/playbook_sbom_creation_v1.yaml @@ -0,0 +1,58 @@ +# Implementation Playbook — curated KNOWLEDGE (the "wie komme ich dort hin?" layer), not runtime code. +# Capability: sbom_creation. Expert FIRST DRAFT — an SBOM practitioner would validate this; it is +# NOT a normative requirement. The renderer (compliance/playbook) assembles it into the journey. + +id: PB-sbom_creation-v1 +capability_id: sbom_creation +status: draft # draft -> reviewed -> validated -> proven +version: 1 + +title: "Software Bill of Materials (SBOM) aufbauen und betreiben" + +why: > + Der CRA verlangt von Herstellern, die Komponenten ihres Produkts zu identifizieren und zu + dokumentieren (Schwachstellenbehandlung, Annex I Teil II). Eine SBOM ist das maschinenlesbare + Inventar aller (auch transitiven) Software-Bestandteile mit Version und Lizenz. Ohne SBOM kann + niemand verlässlich sagen, welche Produkte von einer neuen Schwachstelle (CVE) betroffen sind — + SBOM ist damit die Voraussetzung für Schwachstellenüberwachung, Security-Updates und Meldepflichten. + +tools: + - "CycloneDX (Format, OWASP)" + - "SPDX (Format, Linux Foundation)" + - "Syft (Generator, Container/Filesystem)" + - "cdxgen (Generator, Multi-Ökosystem)" + - "Trivy (Generator + Scan)" + - "OWASP Dependency-Track (Verwaltung + kontinuierliche Überwachung)" + +process_steps: + - title: "Format festlegen" + detail: "CycloneDX oder SPDX wählen (maschinenlesbar). CycloneDX ist im Security-Kontext verbreitet." + - title: "Automatisch im Build erzeugen" + detail: "SBOM-Generierung (Syft/cdxgen/Trivy) als Schritt in die CI/CD-Pipeline einbauen — pro Build, nicht manuell." + - title: "Transitive Abhängigkeiten + Versionen + Lizenzen erfassen" + detail: "Nicht nur direkte Dependencies; gerade transitive Komponenten tragen die meisten CVEs." + - title: "Pro Release versionieren und ablegen" + detail: "Jede ausgelieferte Produktversion bekommt ihre eigene, unveränderliche SBOM (Nachvollziehbarkeit)." + - title: "An Schwachstellenüberwachung anbinden" + detail: "SBOM in Dependency-Track laden -> kontinuierlicher Abgleich gegen neue CVEs/Advisories." + - title: "Release-Gate setzen" + detail: "Auslieferung nur mit gültiger SBOM (Reifegrad-Schritt) — verbindet SBOM mit dem Update-Prozess." + +expected_evidence: + - "Maschinenlesbare SBOM (CycloneDX/SPDX) je ausgelieferter Produktversion" + - "CI-Job-Konfiguration, die die SBOM automatisch erzeugt" + - "Dependency-Track-Projekt (oder gleichwertig) mit laufender Überwachung" + +typical_controls: # INDIKATIV — echte Control-Zuordnung kommt aus der Execution-Schicht + - "component_inventory" + - "supply_chain_risk_management" + +how_others_do_it: > + Verbreitete Praxis: CycloneDX automatisch in der CI via Syft/cdxgen erzeugen und nach + OWASP Dependency-Track pushen, das kontinuierlich gegen neue CVEs prüft. Reifere Organisationen + gaten Releases auf das Vorhandensein einer SBOM und teilen sie auf Anfrage mit Kunden/Behörden. + +disclaimer: > + Kuratiertes Experten-Wissen (Erstentwurf), KEINE normative Anforderung. Werkzeug- und + Schrittempfehlungen sind Beispiele bewährter Praxis, kein Pflichtkatalog. Review durch eine:n + Product-Security-Expert:in ausstehend (status: draft). diff --git a/backend-compliance/knowledge/observations/.gitkeep b/backend-compliance/knowledge/observations/.gitkeep new file mode 100644 index 00000000..66bbe252 --- /dev/null +++ b/backend-compliance/knowledge/observations/.gitkeep @@ -0,0 +1,2 @@ +# Append-only observation log (Task 59b). Real lines (observations.jsonl / YYYY-MM.jsonl) are written at +# runtime via compliance/onboarding/observation_log.py. Anonymised archetypes only — NEVER real company names. diff --git a/backend-compliance/knowledge/onboarding/capability_labels.yaml b/backend-compliance/knowledge/onboarding/capability_labels.yaml new file mode 100644 index 00000000..55b46ee5 --- /dev/null +++ b/backend-compliance/knowledge/onboarding/capability_labels.yaml @@ -0,0 +1,45 @@ +# Human-readable capability labels (DE) — presentation only, reusable across all targets. +# A capability id is the stable machine identity; this maps it to an expert-facing label for the UI. +# Curated knowledge (draft — to be corrected by the domain expert). Missing ids fall back to a +# prettified id in the frontend. NO real company names. Keep labels short + concrete. + +labels: + # ── ISMS / ISO 27001 core ─────────────────────────────────────────────── + information_security_management: "Informationssicherheits-Managementsystem (ISMS)" + access_control_and_authentication: "Zugriffskontrolle & Authentifizierung" + asset_and_configuration_management: "Asset- & Konfigurationsverwaltung" + cryptography: "Kryptographie / Verschlüsselung" + incident_management: "Security-Incident-Management" + security_awareness_training: "Security-Awareness-Schulungen" + supplier_security: "Lieferanten-Sicherheit" + security_logging_and_monitoring: "Security-Logging & Monitoring" + technical_vulnerability_management: "Technisches Schwachstellen-Management" + # ── TISAX / VDA-spezifisch ────────────────────────────────────────────── + prototype_protection: "Prototypenschutz (physisch & logisch)" + tisax_label_scope_selection: "TISAX-Label-/Scope-Festlegung" + tisax_assessment_via_enx: "TISAX-Assessment über die ENX-Plattform" + vda_isa_self_assessment: "VDA-ISA-Selbstauskunft" + data_protection_processing_on_behalf: "Auftragsverarbeitung (Art. 28 DSGVO)" + physical_security: "Physische Sicherheit / Zutrittskontrolle" + # ── QM / ISO 9001 ─────────────────────────────────────────────────────── + document_and_change_control: "Dokumenten- & Änderungslenkung" + supplier_evaluation: "Lieferantenbewertung" + release_and_approval_process: "Freigabe- & Genehmigungsprozess" + ce_conformity_assessment_and_technical_documentation: "CE-Konformitätsbewertung & technische Dokumentation" + # ── CRA / Produkt-Cybersecurity ───────────────────────────────────────── + sbom_creation: "SBOM-Erstellung (Software-Stückliste)" + coordinated_vulnerability_disclosure: "Coordinated Vulnerability Disclosure (CVD)" + secure_development_lifecycle: "Sicherer Entwicklungslebenszyklus (SDLC)" + secure_signed_update_distribution: "Sichere, signierte Update-Verteilung" + security_update_support_period: "Sicherheits-Update-Supportzeitraum" + product_cyber_risk_assessment: "Produkt-Cyber-Risikobewertung" + exploited_vuln_and_incident_reporting: "Meldung ausgenutzter Schwachstellen & Vorfälle" + public_security_advisories: "Öffentliche Security Advisories" + cybersecurity_management_system: "Cybersecurity-Managementsystem (CSMS)" + # ── MaschinenVO / Safety ──────────────────────────────────────────────── + machine_safety_risk_assessment: "Maschinen-Risikobeurteilung" + mechanical_safety_and_guards: "Mechanische Sicherheit & Schutzeinrichtungen" + operating_instructions_and_safety_information: "Betriebsanleitung & Sicherheitshinweise" + protection_against_corruption_of_safety_functions: "Schutz der Sicherheitsfunktionen vor Manipulation" + # ── Umwelt ────────────────────────────────────────────────────────────── + environmental_management_documentation: "Umweltmanagement-Dokumentation" diff --git a/backend-compliance/knowledge/onboarding/intake_signal_map.yaml b/backend-compliance/knowledge/onboarding/intake_signal_map.yaml new file mode 100644 index 00000000..45358848 --- /dev/null +++ b/backend-compliance/knowledge/onboarding/intake_signal_map.yaml @@ -0,0 +1,35 @@ +# Silent Knowledge Pass — signal -> conclusion map (curated DATA, injected). +# +# What a scanner finding lets us conclude WITHOUT asking the user. A signal yields either a capability +# the company demonstrably has (with the evidence already in hand) or a product fact that drives scope. +# `relationship: detected` = a concrete artifact (strong, no question); `partial` = indicative (still +# verify, but lower priority). The scanners (website crawler, repo scanner, doc parser, product intake) +# are UPSTREAM and produce the signals; this file only interprets them. No norm text, no real names. + +mappings: + # Only OBSERVATION-kind signals appear here. requirement-kind signals (sbom_required, psirt_required, + # signed_updates_required) are intentionally ABSENT — they describe a target, never the present state, + # and the Silent Pass would never consume them anyway (it filters on kind == observation). + # ── website ─────────────────────────────────────────────────────────────────────────────── + - {signal: cvd_policy_present, capability: coordinated_vulnerability_disclosure, relationship: detected, evidence: cvd_policy} + - {signal: ce_marking_on_site, capability: ce_conformity_assessment_and_technical_documentation, relationship: partial, evidence: ce_declaration} + - {signal: support_lifecycle_page, capability: security_update_support_period, relationship: partial, evidence: support_policy} + - {signal: security_policy_page, capability: information_security_management, relationship: partial} + # ── repository ──────────────────────────────────────────────────────────────────────────── + - {signal: sbom_present, capability: sbom_creation, relationship: detected, evidence: sbom} + - {signal: signed_releases, capability: secure_signed_update_distribution, relationship: detected, evidence: signing_config} + - {signal: github_actions_ci, capability: secure_development_lifecycle, relationship: partial, evidence: ci_pipeline} + - {signal: dependency_scanning, capability: technical_vulnerability_management, relationship: partial, evidence: vuln_scanning_config} + # ── documents ───────────────────────────────────────────────────────────────────────────── + - {signal: ce_conformity_doc, capability: ce_conformity_assessment_and_technical_documentation, relationship: detected, evidence: technical_documentation} + - {signal: product_risk_assessment_doc, capability: product_cyber_risk_assessment, relationship: detected, evidence: product_risk_assessment} + - {signal: patch_policy_doc, capability: secure_signed_update_distribution, relationship: partial, evidence: patch_policy, + rationale: "indicates update governance, does not evidence signed distribution"} + - {signal: incident_response_plan_doc, capability: incident_management, relationship: detected, evidence: incident_procedure} + # ── product facts (drive scope / target applicability) ────────────────────────────────────── + - {signal: cloud_connectivity, product_fact: connected_to_internet} + - {signal: plc_sps, product_fact: is_machine} + - {signal: embedded_software, product_fact: has_embedded_software} + - {signal: wireless_radio, product_fact: has_radio_equipment} + - {signal: remote_access, product_fact: has_remote_access} + - {signal: generates_usage_data, product_fact: generates_usage_data} diff --git a/backend-compliance/knowledge/onboarding/signal_vocabulary.yaml b/backend-compliance/knowledge/onboarding/signal_vocabulary.yaml new file mode 100644 index 00000000..c7360e5f --- /dev/null +++ b/backend-compliance/knowledge/onboarding/signal_vocabulary.yaml @@ -0,0 +1,44 @@ +# Signal Vocabulary — canonical signal id + aliases + KIND. One language, but TWO kinds of signal. +# +# The same fact ("SBOM present") can arrive as CycloneDX, SPDX, a GitHub Action, a Maven plugin, a +# document upload or a customer statement — for the Silent Pass they are ALL `sbom_present`. This file +# reduces producer dialects to one canonical signal — same pattern as the regulation-alias vocabulary, +# MCAPs and Requirement Sources: many inputs, one language. No scanner-specific logic reaches the Silent +# Pass. Pure DATA, injected into normalize_signals(). No real names. +# +# KIND is the load-bearing distinction (default: observation): +# observation = "I SAW X" — a repo with an SBOM, a published security.txt, a risk-assessment PDF. +# requirement = "someone DEMANDS X" — a tender clause `requires_sbom`, an OEM spec `supplier_requires_psirt`. +# A DEMANDED SBOM is NOT a PRESENT SBOM. `kind` lives on the canonical entry (AUTHORITATIVE), so even a +# mislabelled producer signal cannot collapse the two. The Silent Pass consumes ONLY observations; +# requirement signals are preserved (requirements_seen) and drive the required-set / prioritisation later +# (Requirement Source). This is the Observation-vs-Requirement split the Verification Platform rests on. + +signals: + # ── OBSERVATIONS — "I saw X" (kind: observation, the default) ──────────────────────────────── + - {id: sbom_present, aliases: [cyclonedx_found, spdx_found, sbom_in_repo, sbom_uploaded]} + - {id: cvd_policy_present, aliases: [security_txt, vdp_found, cvd_policy_pdf, psirt_page, coordinated_disclosure_policy]} + - {id: signed_releases, aliases: [signed_artifacts, cosign_found, gpg_signed_releases, code_signing_cert, secure_boot]} + - {id: github_actions_ci, aliases: [ci_pipeline, gitlab_ci, jenkins_pipeline, build_automation]} + - {id: dependency_scanning, aliases: [dependabot, renovate, snyk_found, trivy_in_ci, sca_tool]} + - {id: ce_marking_on_site, aliases: [ce_logo_detected, ce_mark_image]} + - {id: ce_conformity_doc, aliases: [declaration_of_conformity_doc, ce_doc_uploaded, conformity_pdf]} + - {id: support_lifecycle_page, aliases: [eol_policy_page, lifecycle_doc, support_period_stated]} + - {id: security_policy_page, aliases: [isms_statement, iso27001_badge, security_overview_page]} + - {id: product_risk_assessment_doc, aliases: [risk_assessment_pdf, hazard_analysis_doc, tara_doc]} + - {id: patch_policy_doc, aliases: [patch_management_policy, update_policy_pdf]} + - {id: incident_response_plan_doc, aliases: [irp_doc, incident_playbook]} + # product facts (also observations: an observed product property that drives scope) + - {id: cloud_connectivity, aliases: [cloud_hosted, saas, internet_facing, connected_product]} + - {id: plc_sps, aliases: [plc_detected, sps_steuerung, industrial_controller]} + - {id: embedded_software, aliases: [firmware_present, embedded_device]} + - {id: wireless_radio, aliases: [bluetooth, wifi_module, radio_equipment, funkmodul]} + - {id: remote_access, aliases: [remote_maintenance, vpn_access, teleservice, fernwartung]} + - {id: generates_usage_data, aliases: [telemetry_collected, usage_analytics]} + # ── REQUIREMENTS — "someone DEMANDS X" (kind: requirement; NEVER read as present) ───────────── + # Preserved + visible, but the Silent Pass does NOT turn them into detected capabilities. A tender / + # OEM spec / law lands here; a scanner / repo / document lands above. Intentionally UNMAPPED in + # intake_signal_map.yaml — they describe the target, not the present state. + - {id: sbom_required, kind: requirement, aliases: [requires_sbom, sbom_in_tender, tender_requires_sbom]} + - {id: psirt_required, kind: requirement, aliases: [supplier_requires_psirt, requires_psirt, requires_cvd, oem_requires_psirt]} + - {id: signed_updates_required, kind: requirement, aliases: [requires_signed_updates, supplier_requires_signed_updates]} diff --git a/backend-compliance/knowledge/programs/README.md b/backend-compliance/knowledge/programs/README.md new file mode 100644 index 00000000..a9780d82 --- /dev/null +++ b/backend-compliance/knowledge/programs/README.md @@ -0,0 +1,93 @@ +# Domain Knowledge Program — the production line for every domain + +**The architecture is stable. From here the value comes from DOMAIN MODELLING, not more software.** +The real bottleneck is no longer architecture or controls or even „knowledge" — it is **domain +modelling**. Phase B is therefore organised as ONE program with sub-programs per domain, each run +through the SAME production line. No new runtime framework (ADR-008/009, Freeze v1.0). + +## The customer enters by INDUSTRY, not by regulation + +A customer never says „explain ISO 9001". They say „I build packaging machines" / „I'm an automotive +supplier" / „I build parking systems". So the pipeline starts at the industry: + +``` +Industry → Domain Model → Requirement Sources → Requirements → Capabilities → … → Reality / Verification +``` + +## The 7-stage checklist (identical for EVERY domain) + +| # | Stage | Owner | +|---|---|---| +| 1 | **Domain Model** (industry → what world is this?) | Reasoning / curation | +| 2 | **Requirement Sources** (which regulations/standards/specs apply) | Legal Knowledge | +| 3 | **Capability Registry** (capabilities the sources require) | Compliance Execution | +| 4 | **Transition Patterns** (source-state → domain delta) | Reasoning | +| 5 | **Playbooks** (how to implement each capability) | Reasoning | +| 6 | **Reference Scenarios** (canonical regression + expected outcomes) | Reasoning | +| 7 | **Completeness** (auditable coverage per domain) | Reasoning / curation | + +This is the scaling mechanism: every new domain reuses the same production line; the existing engines +(Scope, Gap, Capability Delta, Optimization, Playbooks, Reference, Completeness) extend automatically. + +## A domain knows its typical sources → pre-onboarding HYPOTHESIS (the ETO insight) + +Each domain definition lists `typical_requirement_sources` and `typical_certifications`. So before +onboarding, BreakPilot can say „this process world is *probably* present" — as a **hypothesis, not a +truth**. We don't want to know whether an automotive supplier has ISO 9001 (everyone does); we want +to know **which company capabilities are therefore probably already present** (feeds Company 2A as +`inferred`, never `confirmed`). + +## Per-domain KPI — reproducible, not marketing + +Progress per domain is **derived from the Regulatory Completeness Engine + the actual corpus** +(computed-not-stored): identified requirement sources · modelled capabilities · transition patterns · +playbooks · passed reference scenarios · consciously declared corpus gaps. Rendered as a bar +(`Industrial ███████░░░ 70 %`). These are reproducible quality metrics — no curated numbers. + +## Domain Knowledge Program v1 — backlog (by current customer value) + +| Rank | Domain | File | Typical sources | +|---|---|---|---| +| 1 | **Industrial Automation** | `industrial_automation.yaml` | CRA · MaschinenVO · EMV · RED · Data Act · IEC 62443 · NIS2 | +| 2 | Environmental | `environmental.yaml` | Wasser · Chemikalien · Luft · Energie · Abfall · Produktverantwortung | +| 3 | Automotive | `automotive.yaml` | IATF · TISAX · UNECE R155/R156 · ASPICE · OEM-Lastenhefte | +| 4 | Medical | `medical.yaml` | MDR · IEC 62304 · ISO 14971 | +| 5 | Energy | `energy.yaml` | je nach Zielmarkt | + +The work shifts decisively from software development to knowledge production; the competitive +advantage now comes from the quality and breadth of the modelled domains. + +## The unit of knowledge is the TRANSITION, not the law (Operational Knowledge) + +Customers never ask „what's in the CRA?". They ask **„I have ISO 9001 — what do I still need for the +CRA?"**. The sellable unit is therefore the **transition** (`from → to`), not a single law and not a +single capability. `knowledge/programs/transitions.yaml` is the **Operational Knowledge backlog**: the +~20–30 transitions that are actually demanded (out of the ~N·(N−1) theoretically possible), with a +priority. Nobody buys „EMV domain"; they buy „ISO 9001 → CRA". + +## Three knowledge layers + +``` +Regulatory Knowledge (laws · standards · guidelines) + ↓ +Operational Knowledge (transition patterns · playbooks · capability deltas) ← biggest differentiator + ↓ +Verification Knowledge (source code · SBOM · docs · architecture · processes) ← Vision V2 / Req. Verification +``` + +The middle layer is where we answer not just *what* is required, but *how a company gets there from its +current maturity*. That is the strongest moat. + +## Transition Coverage — a second, stronger KPI + +Besides per-domain maturity, the reference suite reports **Transition Coverage**: per top-transition a +status DERIVED from the transition-pattern corpus (`reviewed/validated/proven → ✅`, `draft → 🟡`, +`none → ⚪`). „ISO 9001 → MaschinenVO ⭐⭐⭐⭐⭐ but ⚪" is a far stronger product indicator than „EMV 30 %". + +## A domain is a TRANSITION PROGRAM with two parallel tracks + +- **Track A — breadth:** model the domain's remaining requirement sources (EMV, RED, IEC 62443, NIS2 …) + so the corpus grows. (Stage 2–3, @Legal-KG / @Execution.) +- **Track B — product:** for each newly modelled source, immediately produce the top transition + patterns + playbooks + reference scenarios → knowledge customers buy and consultants use. (Stage 4–6, + @Reasoning.) Track B turns breadth into product value continuously. diff --git a/backend-compliance/knowledge/programs/automotive.yaml b/backend-compliance/knowledge/programs/automotive.yaml new file mode 100644 index 00000000..3305f4d3 --- /dev/null +++ b/backend-compliance/knowledge/programs/automotive.yaml @@ -0,0 +1,19 @@ +# Domain Knowledge Program — Automotive (backlog rank 3). A domain DEFINITION, not corpus content. +# 7-stage progress is DERIVED from the corpus (computed-not-stored). See programs/README.md. + +id: PROG-automotive +name: "Automotive Domain" +industry: "Automobilzulieferer, OEM-Zulieferkette" +customer_entry: "Ich bin Automobilzulieferer." +backlog_rank: 3 +rationale: "großer Markt; OEM-Lastenhefte = früher Business-Requirement-Anwendungsfall." +status: planned + +typical_requirement_sources: [IATF16949, TISAX, "UNECE R155", "UNECE R156", ASPICE, OEM_Lastenheft] +typical_certifications: [ISO9001, IATF16949, TISAX, ISO27001] + +ownership: "Stufe 1 Reasoning · 2 Legal-KG · 3 Execution · 4-7 Reasoning" +note: > + ISMS→TISAX-Transition-Pattern existiert bereits (Vorarbeit). UNECE R155 (Cybersecurity Management + System) ↔ CRA = quellenübergreifender Convergence-Kandidat. OEM-Lastenheft = erster Business + Requirement (siehe Vision V2 / Requirements Verification, NICHT jetzt). diff --git a/backend-compliance/knowledge/programs/energy.yaml b/backend-compliance/knowledge/programs/energy.yaml new file mode 100644 index 00000000..247d0570 --- /dev/null +++ b/backend-compliance/knowledge/programs/energy.yaml @@ -0,0 +1,18 @@ +# Domain Knowledge Program — Energy (backlog rank 5). A domain DEFINITION, not corpus content. +# 7-stage progress is DERIVED from the corpus (computed-not-stored). See programs/README.md. + +id: PROG-energy +name: "Energy Domain" +industry: "Energieerzeugung/-verteilung, Anlagen kritischer Infrastruktur" +customer_entry: "Ich baue Anlagen für Energieerzeugung / kritische Infrastruktur." +backlog_rank: 5 +rationale: "Zielmarkt-abhängig; nach den klareren Industrie-/Produkt-Domänen." +status: planned + +typical_requirement_sources: [NIS2, IEC62443, CRA, "netzcode/marktabhängig"] +typical_certifications: [ISO27001, IEC62443] + +ownership: "Stufe 1 Reasoning · 2 Legal-KG · 3 Execution · 4-7 Reasoning" +note: > + Stark zielmarkt-abhängig (Netzcodes, nationale Vorgaben). NIS2/IEC62443 teilen sich Capabilities + mit Industrial Automation → Wiederverwendung wahrscheinlich hoch. diff --git a/backend-compliance/knowledge/programs/environmental.yaml b/backend-compliance/knowledge/programs/environmental.yaml new file mode 100644 index 00000000..02ffdb66 --- /dev/null +++ b/backend-compliance/knowledge/programs/environmental.yaml @@ -0,0 +1,33 @@ +# Domain Knowledge Program — Environmental (backlog rank 2). A domain DEFINITION, not corpus content. +# LAW-FIRST: Umweltrecht -> Obligations -> Capabilities -> ISO 14001 -> Delta (never the reverse). +# 7-stage progress is DERIVED from the corpus (computed-not-stored). See programs/README.md. + +id: PROG-environmental +name: "Environmental Domain" +industry: "Hersteller mit Umweltpflichten (z. B. Industriespülmaschinen, Anlagenbau)" +customer_entry: "Welche Umweltanforderungen gelten für mein Produkt (z. B. Industriespülmaschine)?" +backlog_rank: 2 +rationale: "konkreter Kundenbezug (Abwasser/Chemikalien) — direkt nach Industrial Automation." +status: started + +principle: > + ISO 14001 ist KEIN Umweltrecht, sondern ein Managementsystem (= ein Quellzustand). LAW-FIRST: + erst das Recht, dann welche vorhandenen Managementsysteme davon wahrscheinlich schon etwas abdecken. + +# Stage 2 — the requirement-source areas of this domain (each becomes laws/obligations at Stage 2-3) +typical_requirement_sources: [water, chemicals, emissions, energy, waste, product_responsibility] +typical_certifications: [ISO14001, ISO9001] # pre-onboarding capability HYPOTHESIS (nicht Wahrheit) + +# Reasoning capabilities to be modelled (Stage 3, @Execution) once the corpus lands +target_capabilities: + - chemical_management + - wastewater_management + - emissions_monitoring + - hazardous_substance_management + - energy_data_capture + - environmental_incident_management + +ownership: "Stufe 1 Reasoning · 2 Legal-KG (HANDOFF, nur Recht+Pflichten) · 3 Execution (HANDOFF) · 4-7 Reasoning" +note: > + B3 (ISO 14001 -> Korpus-Transition) entsteht ZULETZT, erst wenn Recht + Capabilities bekannt sind. + Acceptance: Regulatory Completeness kippt `Environmental` von unsupported/open auf assessed. diff --git a/backend-compliance/knowledge/programs/industrial_automation.yaml b/backend-compliance/knowledge/programs/industrial_automation.yaml new file mode 100644 index 00000000..9c8a7110 --- /dev/null +++ b/backend-compliance/knowledge/programs/industrial_automation.yaml @@ -0,0 +1,22 @@ +# Domain Knowledge Program — Industrial Automation (backlog rank 1, highest current customer value). +# A domain DEFINITION, not corpus content. The 7-stage progress is DERIVED from the corpus by the +# reference suite (computed-not-stored), never stored here. See programs/README.md for the checklist. + +id: PROG-industrial-automation +name: "Industrial Automation Domain" +industry: "Maschinen-/Anlagenbau, Industrieautomation, Parksysteme, Verpackungsmaschinen" +customer_entry: "Ich baue Verpackungsmaschinen / Parksysteme / Industrieanlagen." +backlog_rank: 1 +rationale: "höchster aktueller Kundennutzen — bereits am weitesten modelliert (CRA + MaschinenVO)." +status: in_progress + +# Stage 2 — the requirement sources that typically apply to this industry +typical_requirement_sources: [CRA, MaschinenVO, EMV, RED, DataAct, IEC62443, NIS2] + +# pre-onboarding capability HYPOTHESIS (nicht Wahrheit, vgl. ETO): feeds Company 2A as `inferred` +typical_certifications: [ISO9001, ISO27001] + +ownership: "Stufe 1 Reasoning · 2 Legal-KG · 3 Execution · 4-7 Reasoning" +note: > + Diese Domäne hat den Vorlauf: CRA + MaschinenVO sind als Convergence-Pattern, RTS und Playbooks + bereits (teilweise) im Korpus. EMV/RED/IEC62443/NIS2 sind identifiziert, aber noch nicht modelliert. diff --git a/backend-compliance/knowledge/programs/medical.yaml b/backend-compliance/knowledge/programs/medical.yaml new file mode 100644 index 00000000..6d85eebe --- /dev/null +++ b/backend-compliance/knowledge/programs/medical.yaml @@ -0,0 +1,18 @@ +# Domain Knowledge Program — Medical (backlog rank 4). A domain DEFINITION, not corpus content. +# 7-stage progress is DERIVED from the corpus (computed-not-stored). See programs/README.md. + +id: PROG-medical +name: "Medical Domain" +industry: "Medizinprodukte-Hersteller, Medizintechnik" +customer_entry: "Ich baue Medizinprodukte / Medizintechnik." +backlog_rank: 4 +rationale: "hoher Leidensdruck (MDR), aber spezialisierter Markt → nach Industrial/Automotive." +status: planned + +typical_requirement_sources: [MDR, IEC62304, ISO14971, IEC60601, CRA] +typical_certifications: [ISO13485, ISO14971] + +ownership: "Stufe 1 Reasoning · 2 Legal-KG · 3 Execution · 4-7 Reasoning" +note: > + IEC 62304 (Software-Lebenszyklus) ↔ CRA secure-development = quellenübergreifender Convergence- + Kandidat. ISO 14971 (Risikomanagement) ↔ Produkt-Risikoanalyse. Erst nach Industrial/Automotive. diff --git a/backend-compliance/knowledge/programs/transitions.yaml b/backend-compliance/knowledge/programs/transitions.yaml new file mode 100644 index 00000000..55f255ab --- /dev/null +++ b/backend-compliance/knowledge/programs/transitions.yaml @@ -0,0 +1,22 @@ +# Operational Knowledge backlog — the TRANSITION is the unit of knowledge, not the law. +# Customers buy „we have ISO 9001, help us with the CRA", not „EMV domain". This is the Operational +# layer (the differentiator). Status per transition is DERIVED from the transition_patterns corpus by +# the reference suite (reviewed/validated/proven -> Gold ✅, draft -> 🟡, none -> ⚪) — computed-not-stored. + +id: BACKLOG-transitions-v1 +note: > + Drei Wissensebenen: Regulatory (Gesetze/Normen/Leitlinien) → OPERATIONAL (diese Transitionen + + Playbooks + Capability-Deltas — der größte Differenzierer) → Verification (Code/SBOM/Doku/Architektur/ + Prozesse, vgl. Vision V2). Diese Liste ist die Operational-Ebene und der eigentliche Verkaufs-Backlog. + +# Theoretisch entstehen aus N Requirement Sources ~N*(N-1) Übergänge; praktisch werden nur ~20-30 +# regelmäßig nachgefragt. Diese hier sind die meistnachgefragten (Priorität 1-5 Sterne). +transitions: + - {from: ISO27001, to: CRA, priority: 5, domains: [industrial_automation]} + - {from: ISO9001, to: CRA, priority: 5, domains: [industrial_automation]} + - {from: ISO9001, to: MaschinenVO, priority: 5, domains: [industrial_automation]} + - {from: IEC62443, to: CRA, priority: 4, domains: [industrial_automation, energy]} + - {from: TISAX, to: CRA, priority: 4, domains: [automotive, industrial_automation]} + - {from: ISO27001, to: NIS2, priority: 4, domains: [industrial_automation, energy]} + - {from: IEC62443, to: NIS2, priority: 4, domains: [industrial_automation, energy]} + - {from: ISO14001, to: Umweltrecht, priority: 3, domains: [environmental]} diff --git a/backend-compliance/knowledge/reference_transition_scenarios/RTS-001.yaml b/backend-compliance/knowledge/reference_transition_scenarios/RTS-001.yaml new file mode 100644 index 00000000..a80c6cff --- /dev/null +++ b/backend-compliance/knowledge/reference_transition_scenarios/RTS-001.yaml @@ -0,0 +1,61 @@ +# Reference Transition Scenario — canonical regression scenario (NOT a test fixture). +# ANONYMIZED ARCHETYPE ONLY — no real company names are stored in the system; illustrative. +# Each RTS pins an Expected Outcome so every commit must reproduce it (identical or better). + +id: RTS-001 +archetype: "Automotive supplier with a mature ISMS — embedded electronics + software, CE products, OEM supply chain" +note: "Anonymized typical starting situation; illustrative only." + +reference_company: + sector: automotive_supply + known_certifications: [TISAX, ISO27001] + product_traits: + is_machine: false # component / embedded supplier + is_component: true + has_embedded_software: true + connected_to_internet: true + has_remote_access: true + generates_usage_data: null # UNKNOWN -> a deciding question, not an assertion + market: [EU] + +transition_goal: + from: [TISAX, ISO27001] + to: + - target: CRA + pattern: TP-ISO27001-CRA-v1 # executed through RS-005 below + - target: MaschinenVO + pattern: null + note: applicability_uncertain # a pure component is usually NOT a machine -> see expected_outcome.maschinenvo + +expected_outcome: + cra: + pattern: TP-ISO27001-CRA-v1 + # The mature ISMS reduces effort here (most info-security capabilities probably covered). + expected_likely_covered_at_least: + - incident_management + - supplier_security + - secure_development_lifecycle + - asset_and_configuration_management + - security_logging_and_monitoring + - access_control_and_authentication + # ... but the CRA product-cyber delta remains. + expected_delta_at_least: + - sbom_creation + - coordinated_vulnerability_disclosure + - security_update_support_period + - secure_signed_update_distribution + - exploited_vuln_and_incident_reporting + - product_cyber_risk_assessment + - ce_conformity_assessment_and_technical_documentation + maschinenvo: + expectation: uncertain # a pure component is usually NOT a machine -> NOT asserted + deciding_questions: [is_machine, is_safety_component, partly_completed_machinery] + rationale: > + The Machinery Regulation applies to machines, safety components and partly completed machinery. + A pure embedded component is usually out of scope, but a SAFETY component is not — so applicability + is itself a deciding question. The engine must ask (is_safety_component?), not assert gilt/gilt-nicht. + Contrast RTS-003, where is_machine: true makes MaschinenVO a settled second target with real convergence. + data_act: + expectation: uncertain # NEVER a fixed gilt/gilt-nicht + deciding_questions: [generates_usage_data, connected_product, data_act_scope] + rationale: "A connected component MAY fall under the Data Act; applicability depends on usage-data generation + scope. The engine must SURFACE this uncertainty and ask, not assert." diff --git a/backend-compliance/knowledge/reference_transition_scenarios/RTS-002.yaml b/backend-compliance/knowledge/reference_transition_scenarios/RTS-002.yaml new file mode 100644 index 00000000..8b2e1822 --- /dev/null +++ b/backend-compliance/knowledge/reference_transition_scenarios/RTS-002.yaml @@ -0,0 +1,60 @@ +# Reference Transition Scenario — ANONYMIZED ARCHETYPE ONLY (no real company names stored). +id: RTS-002 +archetype: "Classic machine builder with only a QMS — precision systems, CE products, no ISMS" +note: "Anonymized typical starting situation; illustrative only. Contrast to RTS-001: a much LARGER delta." + +reference_company: + sector: mechanical_engineering + known_certifications: [ISO9001] + product_traits: + is_machine: true + is_component: false + has_embedded_software: true + connected_to_internet: false # often not connected -> Data Act less likely, still a question + has_remote_access: null + generates_usage_data: null + market: [EU] + +transition_goal: + from: [ISO9001] + to: + - target: CRA + pattern: TP-ISO9001-CRA-v1 + - target: MaschinenVO + pattern: null + note: applies_machine_safety # is_machine: true -> settled second target (machine safety side) + +expected_outcome: + cra: + pattern: TP-ISO9001-CRA-v1 + # A QMS gives only process discipline... + expected_likely_covered_at_least: + - document_and_change_control + - supplier_evaluation + - release_and_approval_process + # ...so the CRA delta is LARGE — nearly the whole security set. + expected_delta_at_least: + - product_cyber_risk_assessment + - secure_development_lifecycle + - technical_vulnerability_management + - coordinated_vulnerability_disclosure + - sbom_creation + - security_update_support_period + - secure_signed_update_distribution + - exploited_vuln_and_incident_reporting + - ce_conformity_assessment_and_technical_documentation + expected_delta_much_larger_than: RTS-001 # regression: ISO9001 leaves more open than ISO27001 + maschinenvo: + expectation: applies # is_machine: true -> settled (not uncertain like RTS-001's component) + expected_delta_at_least: + - machine_safety_risk_assessment + - mechanical_safety_and_guards + - operating_instructions_and_safety_information + low_convergence_note: > + Unlike RTS-003, a QMS-only builder gets almost NO CRA<->MaschinenVO convergence: with no ISMS the + cyber side is entirely in the delta, so few capabilities are shared between the two regulations. + The convergence USP rewards companies that ALREADY have an ISMS — that is the honest contrast. + data_act: + expectation: uncertain + deciding_questions: [connected_product, generates_usage_data, data_act_scope] + rationale: "Often not a connected product, but applicability is not assumed either way — the engine must ask." diff --git a/backend-compliance/knowledge/reference_transition_scenarios/RTS-003.yaml b/backend-compliance/knowledge/reference_transition_scenarios/RTS-003.yaml new file mode 100644 index 00000000..ac844945 --- /dev/null +++ b/backend-compliance/knowledge/reference_transition_scenarios/RTS-003.yaml @@ -0,0 +1,74 @@ +# Reference Transition Scenario — ANONYMIZED ARCHETYPE ONLY (no real company names stored). +id: RTS-003 +archetype: "Machine builder with an ISMS and networked products — connected machines that may generate usage data" +note: "Anonymized typical starting situation; illustrative only. Highlights the Data-Act uncertainty." + +reference_company: + sector: mechanical_engineering + known_certifications: [ISO27001] # ISMS ~ ISO 27001 + product_traits: + is_machine: true + is_component: false + has_embedded_software: true + connected_to_internet: true + has_remote_access: true + generates_usage_data: null # UNKNOWN -> the Data-Act deciding question + market: [EU] + +transition_goal: + from: [ISO27001] + to: + - target: CRA + pattern: TP-ISO27001-CRA-v1 + - target: MaschinenVO + convergence_pattern: TP-ISO27001-CRA-MaschinenVO-v1 # multi-target pattern now exists + note: covered_by_convergence_pattern + - target: DataAct + pattern: null + note: uncertain_hypothesis # NOT asserted — see expected_outcome.data_act + +expected_outcome: + cra: + pattern: TP-ISO27001-CRA-v1 + expected_likely_covered_at_least: + - incident_management + - technical_vulnerability_management + - secure_development_lifecycle + - access_control_and_authentication + - security_logging_and_monitoring + expected_delta_at_least: + - sbom_creation + - coordinated_vulnerability_disclosure + - security_update_support_period + - secure_signed_update_distribution + - exploited_vuln_and_incident_reporting + - product_cyber_risk_assessment + - ce_conformity_assessment_and_technical_documentation + maschinenvo: + # The machine is in scope of the Machinery Regulation (is_machine: true) -> a real second target. + convergence_pattern: TP-ISO27001-CRA-MaschinenVO-v1 + expected_delta_at_least: + - machine_safety_risk_assessment # mechanical safety, ISO 12100 + - mechanical_safety_and_guards + - operating_instructions_and_safety_information + - protection_against_corruption_of_safety_functions # Annex III 1.1.9 = the cyber-safety bridge + convergence: + # The USP: capabilities that satisfy CRA AND MaschinenVO at once (covers_targets [CRA, MaschinenVO]). + convergence_pattern: TP-ISO27001-CRA-MaschinenVO-v1 + targets: [CRA, MaschinenVO] + expected_multi_target_at_least: + - product_cyber_risk_assessment + - protection_against_corruption_of_safety_functions + - secure_signed_update_distribution + - ce_conformity_assessment_and_technical_documentation + rationale: > + ONE capability covers requirements in BOTH regulations — the convergence finding. The engine must + surface these as shared, so the customer sees "N of M new measures satisfy CRA and MaschinenVO at once". + data_act: + expectation: uncertain # the core correction: a connected machine MAY fall under the Data Act + deciding_questions: [generates_usage_data, connected_product, data_act_scope] + rationale: > + A networked machine is MORE likely to fall under the Data Act than a pure component, but it is NOT + a settled fact — it depends on usage-data generation, user access, and scope. The Reference Suite + checks that the engine recognises the RIGHT uncertainty and asks the deciding question, NOT that it + writes a fixed gilt/gilt-nicht. diff --git a/backend-compliance/knowledge/transition_patterns/README.md b/backend-compliance/knowledge/transition_patterns/README.md new file mode 100644 index 00000000..bf066e2c --- /dev/null +++ b/backend-compliance/knowledge/transition_patterns/README.md @@ -0,0 +1,75 @@ +# Regulatory Transition / Convergence Patterns — curated knowledge base + +**Curated regulatory KNOWLEDGE in machine-readable form — not an algorithm, not runtime code.** +This directory holds the Reasoning session's *Knowledge Acquisition* output: versioned, +expert-reviewed patterns describing how to move a company from an **Ausgangszustand** +(e.g. ISO 27001) to a regulatory **Zielzustand** (e.g. CRA). + +Two `pattern_type`s (the term evolves with the scope): +- **`regulatory_transition`** — one source → ONE target regulation (e.g. ISO 27001 → CRA). +- **`regulatory_convergence`** — one source → MULTIPLE targets at once (e.g. ISO 27001 → CRA + MaschinenVO). + Here each capability declares **`covers_targets`** (which regulations it satisfies SIMULTANEOUSLY). + This is the USP: a capability covering >= 2 regulations is *convergence* — + `compliance/transition_reasoning/regulatory_convergence()` counts them, yielding the customer + sentence „von N neuen Maßnahmen erfüllen M gleichzeitig CRA und MaschinenVO". + +Nothing imports these at runtime — they are consumed later by the Transition Planning Engine +(`compliance/transition_reasoning/`, RS-005) and the Question Renderer (RS-005.1). Adding or +curating a pattern is therefore **non-runtime → no deploy** (ADR-001). + +## Maturity levels (replace draft → reviewed → "approved") + +| Level | `status` | Meaning | +|---|---|---| +| 1 | `draft` | AI first draft; no human review. | +| 2 | `reviewed` | Internally reviewed by BreakPilot: architecture consistent, no obvious contradictions, references plausible, reference scenario runs. | +| 3 | `validated` | At least one **domain expert** (e.g. ISO 27001 lead auditor / CRA expert) checked it; all `review_required` points closed. | +| 4 | `proven` | Applied in **multiple real customer projects**; the delta questions proved correct and sufficient; feedback incorporated. | + +"approved" is intentionally NOT used — the target is **validated** (expert-checked), then **proven** (field-tested). + +## Why patterns instead of a question list + +A pattern captures the **difference** between two states, not a full standard: +- `likely_covered` — what the source state probably already establishes (Welt-1 hint, needs + product-level confirmation; never auto-"erfüllt"); +- `delta_requirements` — what the target adds that the source has no analogue for (ask first). + +After ~5 patterns the repeated delta items converge into **Master Delta Questions** — emergent, +not designed up front (the identity-machine discipline of Master Controls/Obligations/Capabilities). + +## File schema (per `transition_pattern__to__v.yaml`) + +`id` · `status` (4 levels above) · `version` · `transition_goal` · `provenance` · `disclaimer` · +`source_state_variants` · `likely_covered[]` · `delta_requirements[]` · `rejected_assumptions[]` · +`determinism_goal` · `review_checklist[]`. + +**`likely_covered[]`** item: `{capability, source_basis, target, relationship` (supports|partially_supports, +never equivalent)`, verification` (required)`, confidence_source` (**relationship** — NOT an LLM estimate; +fits *computed-not-stored*)`, expected_evidence, rationale` (the *Warum*)`, reviewable_claim}`. + +**`delta_requirements[]`** item: `{capability, target_basis, missing_because, why_asked` (customer-facing: +why the source does NOT suffice here → why BreakPilot asks)`, dropped_if` (what makes the question +unnecessary — e.g. a named document/process)`, needed_information` (intent)`, expected_evidence, priority, +reviewable_claim}`. + +## Hard rules + +- **Expert knowledge, not a normative/legal proof.** A pattern is a consultant-grade heuristic; it must + reach `validated` (expert-checked) before customer use. `status` tracks this. +- **Welt-1 only.** „probably covered" is a hint with confidence + verification need, never „erfüllt". +- **Confidence comes from the curated `relationship`, not a model** (`confidence_source: relationship`). +- `question_intent` is an intent (verify_existence / determine_duration / …); the rendered question text + is produced later by RS-005.1, not stored here. +- `capability` ids reference the (Execution-owned) Capability Registry MCAP ids once assigned. + +## Catalogue + +| Pattern | from → to | status (level) | +|---|---|---| +| `transition_pattern_iso27001_to_cra_v1.yaml` | ISO 27001 → CRA | `reviewed` (L2) · transition | +| `transition_pattern_isms_to_tisax_v1.yaml` | ISMS → TISAX | `draft` (L1) · transition | +| `transition_pattern_iso9001_to_cra_v1.yaml` | ISO 9001 → CRA | `draft` (L1) · transition | +| `transition_pattern_iso27001_to_cra_maschinenvo_v1.yaml` | ISO 27001 → CRA + MaschinenVO | `draft` (L1) · **convergence** | + +Next: CRA + MaschinenVO + Data Act (3-target) · `ISO 14001 → environmental regulation` · `ISO 9001 → IATF 16949`. diff --git a/backend-compliance/knowledge/transition_patterns/transition_pattern_isms_to_tisax_v1.yaml b/backend-compliance/knowledge/transition_patterns/transition_pattern_isms_to_tisax_v1.yaml new file mode 100644 index 00000000..32b2bc56 --- /dev/null +++ b/backend-compliance/knowledge/transition_patterns/transition_pattern_isms_to_tisax_v1.yaml @@ -0,0 +1,191 @@ +# Transition KNOWLEDGE Pattern (TKP) — ISMS / ISO 27001 -> TISAX (VDA ISA, ENX) +# Curated regulatory KNOWLEDGE in machine-readable form (not an algorithm, not runtime code). +# Different CHARACTER than ISO27001->CRA: most information-security requirements are likely covered; +# the delta is narrow + automotive-specific (prototype protection, labels, the TISAX assessment process). +# This is the genericity proof for the Transition-Knowledge architecture (not CRA-tailored). + +id: TP-ISMS-TISAX-v1 +status: draft # levels: draft(L1) -> reviewed(L2) -> validated(L3, expert) -> proven(L4, field) +version: 1 + +transition_goal: + from: + standard: "ISO/IEC 27001" + edition: "2022" + nature: organizational_isms + to: + framework: "TISAX" + reference: "VDA ISA (Information Security Assessment), exchanged via ENX" + nature: automotive_supply_chain_infosec + one_line: "Move an organization with an ISMS toward a TISAX label for the automotive supply chain." + +provenance: + author: "Claude (Reasoning session) — AI first draft (L1)" + basis: "ISO/IEC 27001:2022 Annex A vs VDA ISA catalogue (Information Security module ~ ISO 27001; plus Prototype Protection and Data Protection modules) + the TISAX assessment/label process via ENX." + reviewed_by: null + reviewed_at: null + validated_by: null + +disclaimer: > + Curated expert knowledge, NOT a normative proof. KEY INSIGHT: TISAX's Information-Security module + largely OVERLAPS a mature ISO 27001 ISMS — so most is 'supports'. The delta is narrow and + AUTOMOTIVE-specific (prototype protection, the TISAX label scope, and the assessment/exchange process + itself). Every assumption is Welt-1; confidence comes from the curated relationship, not a model. + +source_state_variants: + certified: "ISO 27001 valid + scope covers the relevant sites/teams -> the info-security assumptions hold." + isms_introduced: "ISMS implemented but not certified -> downgrade 'supports' to needs_confirmation; TISAX assessment levels (AL2/AL3) demand evidence depth." + expired: "Certificate lapsed -> re-verify." + limited_scope: "Certified scope excludes the site/team in TISAX scope -> assumptions do NOT transfer for that scope." + +# ── A) LIKELY COVERED — TISAX Information-Security module ~ ISO 27001 (mostly 'supports'). ── +likely_covered: + - capability: information_security_management + iso27001_basis: [Clause_4_10, A.5.1] + tisax_target: [VDA_ISA_information_security] + relationship: supports + confidence_source: relationship + verification: required + expected_evidence: [isms_documentation] + rationale: "A certified ISMS IS the backbone of the TISAX information-security module. Still 'supports', because TISAX may require a higher assessment level (AL2/AL3) and site-specific evidence." + reviewable_claim: "An ISO 27001 ISMS strongly covers the TISAX information-security module but does not by itself grant a label." + + - capability: access_control_and_authentication + iso27001_basis: [A.5.15, A.5.16, A.5.17, A.5.18, A.8.5] + tisax_target: [VDA_ISA_information_security] + relationship: supports + confidence_source: relationship + verification: required + expected_evidence: [access_control_policy] + rationale: "Directly mapped between ISO 27001 and the VDA ISA info-security controls. Hence 'supports'." + reviewable_claim: "Access control maps closely; confirm it covers the TISAX assessment scope." + + - capability: asset_and_configuration_management + iso27001_basis: [A.5.9, A.5.10, A.5.11, A.8.9] + tisax_target: [VDA_ISA_information_security] + relationship: supports + confidence_source: relationship + verification: required + expected_evidence: [asset_inventory] + rationale: "Asset management maps between ISO 27001 and VDA ISA. Hence 'supports'." + reviewable_claim: "Asset management maps closely to the TISAX info-security module." + + - capability: incident_management + iso27001_basis: [A.5.24, A.5.25, A.5.26, A.5.27, A.5.28] + tisax_target: [VDA_ISA_information_security] + relationship: supports + confidence_source: relationship + verification: required + expected_evidence: [incident_response_procedure] + rationale: "Incident management maps between ISO 27001 and VDA ISA. Hence 'supports'." + reviewable_claim: "Incident management maps closely to the TISAX info-security module." + + - capability: supplier_security + iso27001_basis: [A.5.19, A.5.20, A.5.21, A.5.22] + tisax_target: [VDA_ISA_information_security] + relationship: supports + confidence_source: relationship + verification: required + expected_evidence: [supplier_security_policy] + rationale: "Supplier security maps; TISAX adds expectations for connections to OEMs (delta below)." + reviewable_claim: "Supplier security maps; OEM connection specifics are a delta item." + + - capability: cryptography + iso27001_basis: [A.8.24] + tisax_target: [VDA_ISA_information_security] + relationship: supports + confidence_source: relationship + verification: required + expected_evidence: [cryptography_policy] + rationale: "Cryptography maps between ISO 27001 and VDA ISA. Hence 'supports'." + reviewable_claim: "Cryptography maps closely to the TISAX info-security module." + + - capability: physical_security + iso27001_basis: [A.7.1, A.7.2, A.7.3, A.7.4] + tisax_target: [VDA_ISA_information_security] + relationship: partially_supports + confidence_source: relationship + verification: required + expected_evidence: [physical_security_concept] + rationale: "General physical security maps; but TISAX prototype protection demands STRICTER, dedicated physical zones (delta). Hence 'partially_supports'." + reviewable_claim: "General physical security maps, but does not meet prototype-protection zoning." + + - capability: security_awareness_training + iso27001_basis: [A.6.3] + tisax_target: [VDA_ISA_information_security] + relationship: supports + confidence_source: relationship + verification: required + expected_evidence: [training_records] + rationale: "Awareness/training maps between ISO 27001 and VDA ISA. Hence 'supports'." + reviewable_claim: "Awareness training maps closely to the TISAX info-security module." + +# ── B) DELTA — TISAX-specific, NO (or only partial) ISO 27001 analogue. ask first. ── +delta_requirements: + - capability: prototype_protection + tisax_basis: "VDA ISA — Prototype Protection module (physical + logical protection of prototype vehicles/components/parts)." + missing_because: "ISO 27001 has no dedicated prototype-protection requirement." + why_asked: "TISAX (for proto labels) requires dedicated physical/logical protection of prototypes — secure zones, access logging, photography bans, test-drive rules; an ISMS does not cover this." + dropped_if: ["A documented prototype-protection concept with dedicated zones + access control exists.", "The TISAX scope does NOT include a prototype label."] + needed_information: verify_existence + expected_evidence: [prototype_protection_concept] + priority: high + reviewable_claim: "An ISO-27001-only organization typically has no automotive prototype-protection regime." + + - capability: tisax_label_scope_selection + tisax_basis: "TISAX assessment objectives / labels (e.g. info-high, info-very-high, proto-parts, proto-vehicles, data)." + missing_because: "ISO 27001 has no concept of TISAX labels/assessment objectives." + why_asked: "TISAX requires you to choose the assessment objectives (labels) your OEM customers demand; this determines which modules apply." + dropped_if: ["The required TISAX labels are defined (with the customer's demand) and documented."] + needed_information: request_evidence + expected_evidence: [tisax_scope_definition] + priority: high + reviewable_claim: "An ISMS does not define TISAX assessment objectives/labels." + + - capability: vda_isa_self_assessment + tisax_basis: "VDA ISA self-assessment completed before the assessment." + missing_because: "The VDA ISA catalogue self-assessment is TISAX-specific." + why_asked: "TISAX requires a completed VDA ISA self-assessment; an ISO 27001 Statement of Applicability is not the same artefact." + dropped_if: ["A current VDA ISA self-assessment (matching the chosen labels) is completed."] + needed_information: verify_existence + expected_evidence: [vda_isa_self_assessment] + priority: high + reviewable_claim: "An ISO 27001 SoA is not a VDA ISA self-assessment." + + - capability: data_protection_processing_on_behalf + tisax_basis: "VDA ISA — Data Protection module (Art. 28 GDPR processing on behalf), if a 'data' label is in scope." + missing_because: "ISO 27001 is not a data-protection standard." + why_asked: "If a TISAX data label is in scope, you must show Art. 28 GDPR processing-on-behalf controls; ISO 27001 does not establish these." + dropped_if: ["No data label is in scope.", "Art. 28 GDPR processing-on-behalf controls + TOMs are documented."] + needed_information: verify_existence + expected_evidence: [data_processing_agreement, toms] + priority: medium + reviewable_claim: "ISO 27001 does not by itself establish Art. 28 GDPR processing-on-behalf controls." + + - capability: tisax_assessment_via_enx + tisax_basis: "TISAX process: registration on the ENX portal, assessment by an approved audit provider, label exchange via ENX." + missing_because: "The TISAX assessment/exchange process has no ISO 27001 analogue." + why_asked: "A TISAX label is obtained via a specific process (ENX registration + approved audit provider + label exchange); ISO 27001 certification does not produce a TISAX label." + dropped_if: ["A TISAX assessment with an approved audit provider is scheduled/completed and registered on ENX."] + needed_information: verify_existence + expected_evidence: [tisax_assessment_report] + priority: high + reviewable_claim: "ISO 27001 certification does not produce a TISAX label." + +# ── C) Explicit rejections. ── +rejected_assumptions: + - "ISO 27001 does NOT establish automotive prototype protection." + - "ISO 27001 does NOT define TISAX labels / assessment objectives." + - "ISO 27001 does NOT constitute a VDA ISA self-assessment." + - "ISO 27001 certification does NOT produce a TISAX label (separate ENX assessment required)." + - "ISO 27001 does NOT by itself satisfy the Data-Protection module (Art. 28 GDPR) if a data label is in scope." + +determinism_goal: > + Two independent TISAX/ISO 27001 assessors reading this pattern should arrive at the SAME set of delta + questions for the same organization + chosen labels. If not, the pattern is not yet validated. + +review_checklist: + - "Confirm the VDA ISA module mapping (Information Security / Prototype Protection / Data Protection) with a TISAX assessor before customer use." + - "Confirm prototype-protection requirements against the current VDA ISA edition." + - "Replace capability ids with the Capability Registry MCAP ids once assigned (placeholders here)." + - "Confirm which assumptions are level-dependent (AL2 vs AL3)." diff --git a/backend-compliance/knowledge/transition_patterns/transition_pattern_iso13485_to_medical_v1.yaml b/backend-compliance/knowledge/transition_patterns/transition_pattern_iso13485_to_medical_v1.yaml new file mode 100644 index 00000000..9273b1b5 --- /dev/null +++ b/backend-compliance/knowledge/transition_patterns/transition_pattern_iso13485_to_medical_v1.yaml @@ -0,0 +1,82 @@ +# Transition KNOWLEDGE Pattern (TKP) — ISO 13485 (Medical QMS) -> Medical device compliance +# The HARDER scientific test (Phase Ω, test #3). Medical brings, at once, properties not yet jointly +# tested: safety and security TIGHTLY COUPLED, a full product lifecycle, very strong risk-management + +# evidence demands, and high regulatory depth. Sources: MDR, IEC 62304 (software lifecycle), +# ISO 14971 (risk management), IEC 81001-5-1 (health-software security). +# +# KEY for the convergence analysis: IEC 81001-5-1 (health-software security) REQUIRES the same security +# capabilities the CRA does (secure update, vulnerability management, access control, SBOM). So Medical +# REUSES the cyber MCAPs — the safety/security coupling shows up as capability reuse, growing the core +# into a 3rd domain. Alongside that, Medical adds genuinely new caps (clinical evaluation, software +# safety classification, the ISO 14971 risk file, benefit-risk). Capabilities are VERBS. Expert L1 draft. + +id: TP-ISO13485-MEDICAL-v1 +status: draft +version: 1 + +transition_goal: + from: + standard: "ISO 13485" + edition: "2016" + nature: organizational_medical_qms + to: + domain: "Medical device compliance (software-containing device)" + nature: product_safety_and_security + sources: ["MDR", "IEC_62304", "ISO_14971", "IEC_81001_5_1"] + one_line: "Move a manufacturer whose management system is ISO 13485 toward placing a software-containing medical device on the EU market." + +provenance: + author: "Claude (Reasoning session) — AI first draft (L1)" + basis: "ISO 13485:2016 (4.2 documentation, 7.3 design, 7.1/risk, 8.2.1 feedback/PMS, 8.5 CAPA) vs MDR + IEC 62304 + ISO 14971 + IEC 81001-5-1." + reviewed_by: null + validated_by: null + +disclaimer: > + Curated expert knowledge, NOT a normative proof. KEY INSIGHT: ISO 13485 is a medical QUALITY + management system — it gives QMS + design-control + risk-process + PMS discipline, but not the + concrete clinical, software-lifecycle, risk-file or product-security EVIDENCE. Safety and security + are coupled: IEC 81001-5-1 pulls in the SAME security capabilities the CRA requires. Welt-1. + +source_state_variants: + certified: "ISO 13485 certified -> QMS/design/risk-process assumptions hold; concrete evidence still missing." + qms_introduced: "QMS implemented but not certified -> downgrade 'partially_supports' to needs_confirmation." + +# ── A) LIKELY COVERED — medical QMS management discipline (partially_supports), NOT evidence. ── +likely_covered: + - {capability: manage_document_control, iso13485_basis: "4.2", relationship: partially_supports, confidence_source: relationship, verification: required, expected_evidence: [document_control_procedure], rationale: "ISO 13485 document control maintains records but creates no clinical/safety content.", reviewable_claim: "Document control maintains, not creates, medical evidence."} + - {capability: operate_capa_process, iso13485_basis: "8.5", relationship: partially_supports, confidence_source: relationship, verification: required, expected_evidence: [capa_procedure], rationale: "CAPA gives corrective/preventive discipline, not product safety evidence.", reviewable_claim: "CAPA does not establish device safety."} + - {capability: conduct_design_controls, iso13485_basis: "7.3", relationship: partially_supports, confidence_source: relationship, verification: required, expected_evidence: [design_history_file], rationale: "Design controls structure development but do not by themselves produce IEC 62304 software-lifecycle records.", reviewable_claim: "Design controls do not produce the IEC 62304 lifecycle records."} + - {capability: run_risk_management_process, iso13485_basis: "7.1", relationship: partially_supports, confidence_source: relationship, verification: required, expected_evidence: [risk_management_procedure], rationale: "ISO 13485 requires a risk-management PROCESS, but the ISO 14971 risk FILE with concrete analyses is separate.", reviewable_claim: "A risk-management process is not a completed ISO 14971 risk file."} + - {capability: control_suppliers_medical, iso13485_basis: "7.4", relationship: partially_supports, confidence_source: relationship, verification: required, expected_evidence: [supplier_controls], rationale: "Supplier controls give purchasing discipline, not component security.", reviewable_claim: "Medical supplier control does not establish component security."} + - {capability: operate_post_market_surveillance, iso13485_basis: "8.2.1", relationship: partially_supports, confidence_source: relationship, verification: required, expected_evidence: [pms_procedure], rationale: "Feedback/PMS process exists, but MDR PMS + vigilance reporting evidence is separate.", reviewable_claim: "A feedback process is not MDR post-market surveillance evidence."} + +# ── B) DELTA — concrete medical evidence + (coupled) product security. ── +delta_requirements: + # B1) safety/security COUPLING — IEC 81001-5-1 reuses the SAME cyber MCAPs as the CRA: + - {capability: secure_signed_update_distribution, missing_because: "QMS has no secure update.", why_asked: "IEC 81001-5-1 requires authenticity/integrity-protected health-software updates (= CRA).", dropped_if: ["Updates are signed and verified."], needed_information: verify_existence, covers_targets: [IEC_81001_5_1, MDR], expected_evidence: [config_export], priority: high, reviewable_claim: "ISO 13485 does not establish signed update distribution."} + - {capability: technical_vulnerability_management, missing_because: "QMS has no vulnerability management.", why_asked: "IEC 81001-5-1 requires handling health-software vulnerabilities (= CRA).", dropped_if: ["A vulnerability management process exists."], needed_information: verify_existence, covers_targets: [IEC_81001_5_1, MDR], expected_evidence: [vulnerability_management_procedure], priority: high, reviewable_claim: "ISO 13485 does not establish vulnerability management."} + - {capability: access_control_and_authentication, missing_because: "QMS has no product access control.", why_asked: "IEC 81001-5-1 requires authentication/access control for health software (= CRA).", dropped_if: ["Authentication is documented + tested."], needed_information: verify_existence, covers_targets: [IEC_81001_5_1], expected_evidence: [access_control_policy], priority: medium, reviewable_claim: "ISO 13485 does not establish product authentication."} + - {capability: sbom_creation, missing_because: "QMS produces no SBOM.", why_asked: "IEC 81001-5-1 expects a software bill of materials (= CRA).", dropped_if: ["A machine-readable SBOM is produced per release."], needed_information: determine_sbom_maturity, covers_targets: [IEC_81001_5_1], expected_evidence: [sbom], priority: high, reviewable_claim: "ISO 13485 does not produce an SBOM."} + # B2) genuinely NEW medical-specific evidence: + - {capability: conduct_clinical_evaluation, missing_because: "QMS produces no clinical evidence.", why_asked: "The MDR requires a clinical evaluation / clinical evidence.", dropped_if: ["A clinical evaluation report exists."], needed_information: request_evidence, covers_targets: [MDR], expected_evidence: [clinical_evaluation_report], priority: high, reviewable_claim: "ISO 13485 does not produce clinical evidence."} + - {capability: classify_software_safety_iec62304, missing_because: "QMS does not classify software safety.", why_asked: "IEC 62304 requires a software safety classification (Class A/B/C) driving the lifecycle.", dropped_if: ["A software safety classification exists."], needed_information: request_evidence, covers_targets: [IEC_62304], expected_evidence: [software_safety_classification], priority: high, reviewable_claim: "ISO 13485 does not classify software safety per IEC 62304."} + - {capability: maintain_risk_management_file_iso14971, missing_because: "QMS has a process, not the file.", why_asked: "ISO 14971 requires a maintained risk management file with concrete analyses + controls.", dropped_if: ["An ISO 14971 risk management file exists."], needed_information: request_evidence, covers_targets: [ISO_14971, MDR], expected_evidence: [risk_management_file], priority: high, reviewable_claim: "An ISO 13485 risk process is not a completed ISO 14971 risk file."} + - {capability: perform_benefit_risk_analysis, missing_because: "QMS does not weigh benefit vs risk.", why_asked: "The MDR + ISO 14971 require a documented benefit-risk determination.", dropped_if: ["A benefit-risk analysis exists."], needed_information: request_evidence, covers_targets: [MDR, ISO_14971], expected_evidence: [benefit_risk_analysis], priority: medium, reviewable_claim: "ISO 13485 does not document a benefit-risk analysis."} + - {capability: implement_software_lifecycle_iec62304, missing_because: "QMS has design controls, not the IEC 62304 lifecycle.", why_asked: "IEC 62304 requires a defined software development + maintenance lifecycle.", dropped_if: ["IEC 62304 lifecycle records exist."], needed_information: request_evidence, covers_targets: [IEC_62304], expected_evidence: [software_lifecycle_records], priority: high, reviewable_claim: "ISO 13485 does not produce IEC 62304 lifecycle records."} + - {capability: assign_unique_device_identification, missing_because: "QMS has no UDI.", why_asked: "The MDR requires UDI assignment + registration.", dropped_if: ["UDI is assigned and registered."], needed_information: verify_existence, covers_targets: [MDR], expected_evidence: [udi_record], priority: medium, reviewable_claim: "ISO 13485 does not assign UDI."} + - {capability: compile_medical_technical_documentation, missing_because: "QMS has no MDR technical documentation.", why_asked: "The MDR (Annex II/III) requires complete technical documentation + DoC.", dropped_if: ["MDR technical documentation exists."], needed_information: request_evidence, covers_targets: [MDR], expected_evidence: [technical_documentation, declaration_of_conformity], priority: high, reviewable_claim: "ISO 13485 does not satisfy MDR technical documentation."} + +rejected_assumptions: + - "ISO 13485 does NOT produce clinical evidence or a clinical evaluation." + - "ISO 13485 does NOT produce the ISO 14971 risk management FILE (only the process)." + - "ISO 13485 does NOT produce IEC 62304 software-lifecycle records or a software safety classification." + - "ISO 13485 does NOT establish health-software security (IEC 81001-5-1 = the same security caps as the CRA)." + +determinism_goal: > + Two independent auditors should agree that an ISO-13485-only manufacturer has medical QMS discipline + but is missing the clinical, software-lifecycle, risk-file and product-security evidence — and that the + security part is the SAME capability set as the CRA (safety/security coupling). + +review_checklist: + - "Confirm delta + rejected_assumptions with a medical regulatory expert." + - "Replace capability ids with Capability Registry MCAP ids once assigned." diff --git a/backend-compliance/knowledge/transition_patterns/transition_pattern_iso14001_to_environmental_v1.yaml b/backend-compliance/knowledge/transition_patterns/transition_pattern_iso14001_to_environmental_v1.yaml new file mode 100644 index 00000000..365238f4 --- /dev/null +++ b/backend-compliance/knowledge/transition_patterns/transition_pattern_iso14001_to_environmental_v1.yaml @@ -0,0 +1,118 @@ +# Transition KNOWLEDGE Pattern (TKP) — ISO 14001 (EMS) -> Environmental / Material compliance +# THE FIRST NON-CYBER STRESS TEST. Every prior pattern lives in the cyber family (infosec / software / +# product cybersecurity). Environmental brings entirely different mental models: substance flows, +# emissions, water, chemicals, energy, circularity, disposal. If RS-005 carries this UNCHANGED (only new +# DATA, zero runtime code), the architecture is general beyond cyber. +# +# Same shape as ISO 9001 -> CRA: ISO 14001 is a MANAGEMENT system. It gives environmental management +# discipline (aspects, compliance process, audits, improvement, document control) but NOT the concrete, +# substance-/product-specific EVIDENCE. So the delta is large, and the new quality question is explicit: +# "which environmental capabilities does ISO 14001 typically NOT produce?" -> rejected_assumptions. +# Capabilities are VERBS (capability-is-a-verb). Curated expert FIRST DRAFT, NOT a normative proof. + +id: TP-ISO14001-ENV-v1 +status: draft # draft(L1) -> reviewed(L2) -> validated(L3, expert) -> proven(L4) +version: 1 + +transition_goal: + from: + standard: "ISO 14001" + edition: "2015" + nature: organizational_environmental_management_system + to: + domain: "Environmental / Material compliance" + nature: concrete_environmental_evidence + sources: ["REACH", "RoHS", "Batterieverordnung", "Wasserrecht", "Abwasservorschriften", "Energiemanagement (EnEfG)", "Kreislaufwirtschaft (KrWG/AVV)", "Emissionsschutz (BImSchG)"] + one_line: "Move a manufacturer whose only environmental management system is ISO 14001 toward concrete environmental/material compliance for a product placed on the EU market." + +provenance: + author: "Claude (Reasoning session) — AI first draft (L1)" + basis: "ISO 14001:2015 (6.1.2 aspects, 6.1.3 compliance obligations, 7.5 documented information, 9.2 internal audit, 10.3 continual improvement) vs concrete substance/emission/water/material duties." + reviewed_by: null + validated_by: null + +disclaimer: > + Curated expert knowledge, NOT a normative proof. KEY INSIGHT: ISO 14001 is an environmental MANAGEMENT + system — it provides the discipline to identify aspects and run compliance/audit/improvement processes, + but it produces NO concrete substance lists, emission measurements, REACH registrations, battery + passports or water analyses. The environmental delta for an ISO-14001-only manufacturer is therefore + LARGE. Welt-1; confidence from the curated relationship, never "erfüllt". + +source_state_variants: + certified: "ISO 14001 certified -> the management-discipline assumptions hold; concrete evidence is still missing." + ems_introduced: "EMS implemented but not certified -> downgrade 'partially_supports' to needs_confirmation." + +# ── A) LIKELY COVERED — only environmental MANAGEMENT discipline (partially_supports), NOT evidence. ── +likely_covered: + - capability: identify_environmental_aspects + iso14001_basis: ["6.1.2"] + relationship: partially_supports + confidence_source: relationship + verification: required + expected_evidence: [environmental_aspects_register] + rationale: "ISO 14001 requires identifying environmental aspects/impacts — the discipline to KNOW where chemicals, water, energy and waste are relevant — but not the concrete substance/emission data itself." + reviewable_claim: "Aspect identification scopes environmental topics but does not measure or declare any substance." + - capability: operate_environmental_compliance_process + iso14001_basis: ["6.1.3", "9.1.2"] + relationship: partially_supports + confidence_source: relationship + verification: required + expected_evidence: [compliance_obligations_register] + rationale: "ISO 14001 requires a process to determine and evaluate compliance obligations — a framework to TRACK duties, not to discharge any specific one." + reviewable_claim: "A compliance-obligations process tracks duties but does not produce a REACH registration or an emission report." + - capability: conduct_internal_environmental_audits + iso14001_basis: ["9.2"] + relationship: partially_supports + confidence_source: relationship + verification: required + expected_evidence: [internal_audit_programme] + rationale: "Internal audit gives assurance that the EMS runs — process assurance, not substance evidence." + reviewable_claim: "Internal audits assure the management system, not concrete environmental performance." + - capability: run_continual_environmental_improvement + iso14001_basis: ["10.3"] + relationship: partially_supports + confidence_source: relationship + verification: required + expected_evidence: [improvement_objectives] + rationale: "Continual improvement drives objectives/targets — direction, not the concrete deliverables a regulation demands." + reviewable_claim: "Improvement objectives set direction but do not constitute regulatory evidence." + - capability: control_environmental_documents + iso14001_basis: ["7.5"] + relationship: partially_supports + confidence_source: relationship + verification: required + expected_evidence: [document_control_procedure] + rationale: "Documented-information control gives the discipline to MAINTAIN records — but no record content." + reviewable_claim: "Document control maintains records; it does not create the substance/emission records themselves." + +# ── B) DELTA — the concrete substance/emission/water/material EVIDENCE ISO 14001 does NOT produce. ── +# Each carries covers_targets = the requirement sources that demand it (the verb -> sources mapping). +delta_requirements: + - {capability: manage_chemical_substances, missing_because: "An EMS does not maintain a concrete chemical inventory.", why_asked: "REACH/RoHS require knowing exactly which substances are present.", dropped_if: ["A maintained substance inventory exists."], needed_information: verify_existence, covers_targets: [REACH, RoHS], expected_evidence: [chemical_inventory], priority: high, reviewable_claim: "ISO 14001 does not maintain a concrete substance inventory."} + - {capability: register_substances_under_reach, missing_because: "No REACH registration dossiers in an EMS.", why_asked: "REACH requires registration of manufactured/imported substances >1 t/a.", dropped_if: ["REACH registration/notification dossiers exist."], needed_information: request_evidence, covers_targets: [REACH], expected_evidence: [reach_registration_dossier], priority: high, reviewable_claim: "ISO 14001 does not produce REACH registrations."} + - {capability: restrict_hazardous_substances_rohs, missing_because: "No RoHS substance-restriction evidence in an EMS.", why_asked: "RoHS restricts specific hazardous substances in EEE.", dropped_if: ["RoHS compliance declarations + material data exist."], needed_information: request_evidence, covers_targets: [RoHS], expected_evidence: [rohs_declaration], priority: high, reviewable_claim: "ISO 14001 does not establish RoHS substance-restriction evidence."} + - {capability: monitor_water_consumption, missing_because: "An EMS does not meter water by permit.", why_asked: "Water permits require monitoring abstraction/consumption.", dropped_if: ["Water consumption is metered and reported per permit."], needed_information: verify_existence, covers_targets: ["Wasserrecht"], expected_evidence: [water_consumption_records], priority: medium, reviewable_claim: "ISO 14001 does not meter water consumption per permit."} + - {capability: treat_and_document_wastewater, missing_because: "No concrete effluent treatment/analysis in an EMS.", why_asked: "National wastewater rules set discharge limits + monitoring.", dropped_if: ["Effluent treatment + discharge analyses exist."], needed_information: request_evidence, covers_targets: ["Abwasservorschriften"], expected_evidence: [wastewater_analysis], priority: high, reviewable_claim: "ISO 14001 does not treat or analyse wastewater."} + - {capability: account_energy_consumption, missing_because: "No concrete energy accounting in an EMS.", why_asked: "Energy-management duties require documented consumption.", dropped_if: ["Energy consumption is accounted and reported."], needed_information: verify_existence, covers_targets: ["Energiemanagement (EnEfG)"], expected_evidence: [energy_consumption_report], priority: medium, reviewable_claim: "ISO 14001 does not account energy consumption."} + - {capability: document_waste_streams, missing_because: "No concrete waste-stream records in an EMS.", why_asked: "Circular-economy/waste law requires documented streams + codes.", dropped_if: ["Waste streams are documented with EWC codes."], needed_information: verify_existence, covers_targets: ["Kreislaufwirtschaft (KrWG/AVV)"], expected_evidence: [waste_register], priority: medium, reviewable_claim: "ISO 14001 does not document concrete waste streams."} + - {capability: declare_material_composition, missing_because: "No material declaration in an EMS.", why_asked: "Customer/SCIP/battery rules require material declarations.", dropped_if: ["Material declarations (e.g. SCIP) exist."], needed_information: request_evidence, covers_targets: ["Kundenanforderungen", "Batterieverordnung"], expected_evidence: [material_declaration], priority: high, reviewable_claim: "ISO 14001 does not declare material composition."} + - {capability: issue_battery_passport, missing_because: "No battery passport in an EMS.", why_asked: "The Battery Regulation requires a battery passport for in-scope batteries.", dropped_if: ["A battery passport is issued per unit/model."], needed_information: request_evidence, covers_targets: ["Batterieverordnung"], expected_evidence: [battery_passport], priority: high, reviewable_claim: "ISO 14001 does not produce a battery passport."} + - {capability: measure_air_emissions, missing_because: "No concrete emission measurements in an EMS.", why_asked: "Emission-protection law requires measured emissions for in-scope installations.", dropped_if: ["Emission measurements/reports exist per permit."], needed_information: request_evidence, covers_targets: ["Emissionsschutz (BImSchG)"], expected_evidence: [emission_measurement_report], priority: medium, reviewable_claim: "ISO 14001 does not measure air emissions."} + - {capability: analyze_water_discharge, missing_because: "No concrete water analyses in an EMS.", why_asked: "Permits require periodic water/effluent analyses.", dropped_if: ["Periodic water analyses exist."], needed_information: request_evidence, covers_targets: ["Wasserrecht", "Abwasservorschriften"], expected_evidence: [water_analysis], priority: medium, reviewable_claim: "ISO 14001 does not perform water analyses."} + +# ── C) REJECTED ASSUMPTIONS — the new quality question: what ISO 14001 typically does NOT produce. ── +rejected_assumptions: + - "ISO 14001 does NOT produce concrete substance lists or REACH registrations." + - "ISO 14001 does NOT produce concrete air-emission measurements." + - "ISO 14001 does NOT produce battery passports or material declarations." + - "ISO 14001 does NOT produce water or wastewater analyses." + - "An ISO 14001 certificate does NOT establish RoHS substance-restriction evidence." + +determinism_goal: > + Two independent auditors should agree that an ISO-14001-only manufacturer has the environmental + MANAGEMENT discipline but is missing nearly all concrete substance/emission/water/material evidence — + the same shape as ISO 9001 -> CRA, in a completely non-cyber domain. + +review_checklist: + - "Confirm the delta + rejected_assumptions with an environmental compliance expert." + - "Replace capability ids with Capability Registry MCAP ids once assigned." diff --git a/backend-compliance/knowledge/transition_patterns/transition_pattern_iso27001_to_cra_maschinenvo_v1.yaml b/backend-compliance/knowledge/transition_patterns/transition_pattern_iso27001_to_cra_maschinenvo_v1.yaml new file mode 100644 index 00000000..7b23cc82 --- /dev/null +++ b/backend-compliance/knowledge/transition_patterns/transition_pattern_iso27001_to_cra_maschinenvo_v1.yaml @@ -0,0 +1,84 @@ +# Regulatory CONVERGENCE Pattern (the multi-regulation evolution of a Transition Pattern). +# ISO/IEC 27001 (ISMS) -> CRA + MaschinenVO (two target regulations at once). +# The NEW thing: each capability declares `covers_targets` -> which regulations it satisfies +# SIMULTANEOUSLY. This is the convergence USP: "X capabilities cover both CRA and MaschinenVO". +# Curated knowledge, not runtime code. + +id: TP-ISO27001-CRA-MaschinenVO-v1 +pattern_type: regulatory_convergence # vs. single-target regulatory_transition +status: draft # draft -> reviewed -> validated -> proven +version: 1 + +transition_goal: + from: + standard: "ISO/IEC 27001" + edition: "2022" + nature: organizational_isms + to: # TWO targets + - regulation: "Cyber Resilience Act" + reference: "Regulation (EU) 2024/2847" + applies_from: "2027-12-11" + - regulation: "Maschinenverordnung" + reference: "Regulation (EU) 2023/1230" + applies_from: "2027-01-20" + one_line: "A connected machine builder with an ISMS moving toward BOTH CRA and the Machinery Regulation." + +provenance: + author: "Claude (Reasoning session) — AI first draft (L1)" + basis: "ISO/IEC 27001:2022 vs CRA Annex I + Maschinenverordnung (EU) 2023/1230 (Annex III EHSR, esp. 1.1.9 'protection against corruption' = the cyber-safety bridge; risk assessment; technical documentation Annex IV; conformity assessment + CE)." + reviewed_by: null + validated_by: null + +disclaimer: > + Curated expert knowledge, NOT a normative proof. The convergence (which capability covers which + regulation) is a curated relationship, not a model estimate. CRA = product cybersecurity; + MaschinenVO = machine safety WITH a cyber-safety bridge (Annex III 1.1.9 protection against + corruption of safety functions). The two overlap precisely where cybersecurity meets safety. + +source_state_variants: + certified: "ISO 27001 valid + scope covers the product -> the info-security assumptions hold." + isms_introduced: "ISMS not certified -> downgrade 'supports' to needs_confirmation." + +# A) LIKELY COVERED — the ISMS mostly supports the CRA side; MaschinenVO machine safety is not covered. +likely_covered: + - {capability: incident_management, relationship: supports, confidence_source: relationship, verification: required, covers_targets: [CRA], rationale: "ISMS incident management supports CRA handling; MaschinenVO safety is separate."} + - {capability: technical_vulnerability_management, relationship: supports, confidence_source: relationship, verification: required, covers_targets: [CRA], rationale: "Internal vuln mgmt supports CRA; not a MaschinenVO requirement."} + - {capability: access_control_and_authentication, relationship: supports, confidence_source: relationship, verification: required, covers_targets: [CRA, MaschinenVO], rationale: "Access control supports CRA auth AND MaschinenVO protection of safety functions against unauthorized change."} + - {capability: secure_development_lifecycle, relationship: supports, confidence_source: relationship, verification: required, covers_targets: [CRA], rationale: "Secure dev supports CRA secure-by-design."} + - {capability: security_logging_and_monitoring, relationship: supports, confidence_source: relationship, verification: required, covers_targets: [CRA, MaschinenVO], rationale: "Logging supports CRA security events AND MaschinenVO traceability of safety-relevant events."} + +# B) DELTA — each carries covers_targets. The CONVERGENCE items cover BOTH regulations. +delta_requirements: + # --- CRA-only --- + - {capability: sbom_creation, covers_targets: [CRA], why_asked: "CRA requires an SBOM; MaschinenVO does not.", dropped_if: ["A machine-readable SBOM per release exists."], needed_information: determine_sbom_maturity, expected_evidence: [sbom], priority: high} + - {capability: coordinated_vulnerability_disclosure, covers_targets: [CRA], why_asked: "CRA requires a CVD/PSIRT; not a MaschinenVO requirement.", dropped_if: ["A published CVD policy + contact exists."], needed_information: verify_existence, expected_evidence: [cvd_policy], priority: high} + - {capability: exploited_vuln_and_incident_reporting, covers_targets: [CRA], why_asked: "CRA Art. 14 authority reporting; not in MaschinenVO.", dropped_if: ["An Art. 14 reporting procedure exists."], needed_information: verify_existence, expected_evidence: [reporting_procedure], priority: high} + - {capability: security_update_support_period, covers_targets: [CRA], why_asked: "CRA support period; MaschinenVO has no equivalent.", dropped_if: ["A support/lifecycle policy defines the period."], needed_information: determine_duration, expected_evidence: [support_policy], priority: high} + - {capability: public_security_advisories, covers_targets: [CRA], why_asked: "CRA advisories; not in MaschinenVO.", dropped_if: ["An advisory process exists."], needed_information: verify_existence, expected_evidence: [advisory_process], priority: medium} + # --- CONVERGENCE: cover BOTH CRA and MaschinenVO --- + - {capability: product_cyber_risk_assessment, covers_targets: [CRA, MaschinenVO], why_asked: "Both require assessing cyber threats to the product — CRA as product cyber risk, MaschinenVO Annex III 1.1.9 as protection of safety functions against corruption. ONE assessment can serve both.", dropped_if: ["A combined product cyber + safety risk assessment exists."], needed_information: verify_existence, expected_evidence: [product_risk_assessment], priority: high} + - {capability: protection_against_corruption_of_safety_functions, covers_targets: [CRA, MaschinenVO], why_asked: "MaschinenVO Annex III 1.1.9 requires safety functions to resist corruption; CRA requires integrity protection. ONE control set covers both.", dropped_if: ["Safety-critical software/data integrity protection is documented + tested."], needed_information: verify_existence, expected_evidence: [test_report, config_export], priority: high} + - {capability: secure_signed_update_distribution, covers_targets: [CRA, MaschinenVO], why_asked: "CRA requires secure updates; MaschinenVO requires that updates not compromise safety. Signed, integrity-checked updates serve both.", dropped_if: ["Updates are signed + verified and safety-impact-assessed."], needed_information: verify_existence, expected_evidence: [config_export, test_report], priority: high} + - {capability: ce_conformity_assessment_and_technical_documentation, covers_targets: [CRA, MaschinenVO], why_asked: "Both require a conformity assessment + CE + technical documentation + DoC for the same product; the dossier discipline converges (content differs).", dropped_if: ["A combined technical documentation set covering CRA + MaschinenVO exists."], needed_information: request_evidence, expected_evidence: [technical_documentation, declaration_of_conformity], priority: high} + # --- MaschinenVO-only --- + - {capability: machine_safety_risk_assessment, covers_targets: [MaschinenVO], why_asked: "MaschinenVO requires a machine safety risk assessment (mechanical hazards, ISO 12100); CRA does not.", dropped_if: ["A machine safety risk assessment per ISO 12100 exists."], needed_information: verify_existence, expected_evidence: [machine_risk_assessment], priority: high} + - {capability: mechanical_safety_and_guards, covers_targets: [MaschinenVO], why_asked: "MaschinenVO essential health & safety requirements (guards, emergency stop, stability); not a CRA topic.", dropped_if: ["Mechanical safety design + guards are documented."], needed_information: verify_existence, expected_evidence: [safety_design_documentation], priority: high} + - {capability: operating_instructions_and_safety_information, covers_targets: [MaschinenVO], why_asked: "MaschinenVO requires operating instructions + safety information; CRA does not.", dropped_if: ["Compliant operating instructions + safety information exist."], needed_information: verify_existence, expected_evidence: [operating_instructions], priority: medium} + +rejected_assumptions: + - "ISO 27001 does NOT establish machine safety (risk assessment, guards, instructions)." + - "ISO 27001 does NOT establish the CRA product-cyber delta (SBOM, CVD, support period, Art. 14)." + - "A CRA cyber risk assessment is NOT automatically a MaschinenVO safety risk assessment (but the cyber-safety bridge converges on protection against corruption)." + +convergence_note: > + The capabilities tagged covers_targets [CRA, MaschinenVO] are the convergence: ONE capability that + satisfies requirements in BOTH regulations at once. This is the basis for the customer sentence + „von N neuen Maßnahmen erfüllen M gleichzeitig CRA und MaschinenVO". + +determinism_goal: > + Two independent CRA + MaschinenVO experts should agree on the covers_targets split for each capability. + +review_checklist: + - "Confirm the cyber-safety bridge (Annex III 1.1.9) mapping with a machinery safety expert." + - "Confirm each covers_targets assignment ([CRA] / [MaschinenVO] / [CRA, MaschinenVO])." + - "Replace capability ids with Capability Registry MCAP ids once assigned." diff --git a/backend-compliance/knowledge/transition_patterns/transition_pattern_iso27001_to_cra_v1.yaml b/backend-compliance/knowledge/transition_patterns/transition_pattern_iso27001_to_cra_v1.yaml new file mode 100644 index 00000000..7004389b --- /dev/null +++ b/backend-compliance/knowledge/transition_patterns/transition_pattern_iso27001_to_cra_v1.yaml @@ -0,0 +1,232 @@ +# Transition KNOWLEDGE Pattern (TKP) — ISO/IEC 27001 (ISMS) -> CRA (Reg. (EU) 2024/2847) +# Curated regulatory KNOWLEDGE in machine-readable form (not an algorithm, not runtime code). +# Gold-standard template; consumed later by the Transition Planning Engine (RS-005) + Renderer (RS-005.1). + +id: TP-ISO27001-CRA-v1 +status: reviewed # levels: draft(L1) -> reviewed(L2) -> validated(L3, expert) -> proven(L4, field) +version: 1 + +transition_goal: + from: + standard: "ISO/IEC 27001" + edition: "2022" + nature: organizational_isms + to: + regulation: "Cyber Resilience Act" + reference: "Regulation (EU) 2024/2847" + applies_from: "2027-12-11" + nature: product_cybersecurity + one_line: "Move a manufacturer with a certified ISMS toward CRA conformity for a product with digital elements." + +provenance: + author: "Claude (Reasoning session) — AI first draft, internally reviewed (L2)" + basis: "ISO/IEC 27001:2022 Annex A vs CRA Annex I (Part I security requirements, Part II vulnerability handling), Art. 13 (support period), Art. 14 (reporting), conformity assessment + CE + technical documentation (Annex VII)." + reviewed_by: "BreakPilot (internal, architecture + structure)" + reviewed_at: null + validated_by: null # domain expert (ISO 27001 lead auditor / CRA expert) — pending + +disclaimer: > + Curated expert knowledge, NOT a normative or legal proof. KEY INSIGHT (drives ~80% of decisions): + ISO 27001 is ORGANIZATIONAL (an ISMS); the CRA is PRODUCT-level. Every assumption is Welt-1 — a hint + needing PRODUCT-level evidence, never automatically "erfüllt". Confidence comes from the curated + relationship, not a model estimate. Each line is phrased so an auditor can AGREE or DISAGREE. + +source_state_variants: + certified: "ISO 27001 certificate valid AND scope covers the product's development/operations -> assumptions hold at the stated relationship." + isms_introduced: "ISMS implemented but not certified -> downgrade every 'supports' to needs_confirmation." + expired: "Certificate lapsed -> re-verify; treat all assumptions as needs_confirmation." + limited_scope: "Certified scope excludes the product or its dev unit -> assumptions do NOT transfer; treat as missing until scope is confirmed." + +# ── A) LIKELY COVERED (org level). relationship + confidence_source + verification + rationale. ── +likely_covered: + - capability: incident_management + iso27001_basis: [A.5.24, A.5.25, A.5.26, A.5.27, A.5.28] + cra_target: [Annex_I_Part_II, Art_14_reporting] + relationship: supports + confidence_source: relationship # curated, NOT an LLM estimate (computed-not-stored) + verification: required + expected_evidence: [incident_response_procedure] + rationale: "ISO 27001 covers ORGANIZATIONAL incident management; the CRA additionally demands PRODUCT vulnerability handling and statutory reporting to CSIRT/ENISA. Hence 'supports', not 'equivalent'." + reviewable_claim: "A certified ISMS provides an incident-management foundation but does NOT by itself satisfy CRA product incident handling or Art. 14 reporting." + + - capability: technical_vulnerability_management + iso27001_basis: [A.8.8] + cra_target: [Annex_I_Part_II_2] + relationship: supports + confidence_source: relationship + verification: required + expected_evidence: [vulnerability_management_procedure] + rationale: "Internal vulnerability management exists; the CRA needs a PRODUCT-facing process (PSIRT, coordinated disclosure). Hence 'supports'." + reviewable_claim: "Internal vulnerability management does NOT establish a product PSIRT or CVD." + + - capability: supplier_security + iso27001_basis: [A.5.19, A.5.20, A.5.21, A.5.22] + cra_target: [Annex_I_Part_II_1_components] + relationship: supports + confidence_source: relationship + verification: required + expected_evidence: [supplier_security_policy] + rationale: "Supplier security exists; the CRA needs component-level transparency (SBOM) and tracking third-party/OSS vulnerabilities. Hence 'supports'." + reviewable_claim: "Supplier security does NOT establish an SBOM or component vulnerability tracking." + + - capability: access_control_and_authentication + iso27001_basis: [A.5.15, A.5.16, A.5.17, A.5.18, A.8.5] + cra_target: [Annex_I_Part_I_4] + relationship: supports + confidence_source: relationship + verification: required + expected_evidence: [access_control_policy] + rationale: "Org access control is mature; the CRA needs product authentication and no default credentials. Hence 'supports'." + reviewable_claim: "Org access control does NOT guarantee product authentication or absence of default credentials." + + - capability: cryptography + iso27001_basis: [A.8.24] + cra_target: [Annex_I_Part_I_5, Annex_I_Part_I_6] + relationship: partially_supports + confidence_source: relationship + verification: required + expected_evidence: [cryptography_policy] + rationale: "A crypto policy exists; whether the PRODUCT actually encrypts data and protects integrity is unproven. Hence 'partially_supports'." + reviewable_claim: "A crypto policy does NOT prove product-level confidentiality/integrity." + + - capability: security_logging_and_monitoring + iso27001_basis: [A.8.15, A.8.16] + cra_target: [Annex_I_Part_I_12] + relationship: supports + confidence_source: relationship + verification: required + expected_evidence: [logging_concept] + rationale: "Org logging exists; the CRA needs security-relevant events recorded ON the product (with opt-out). Hence 'supports'." + reviewable_claim: "Org logging does NOT prove product security-event logging." + + - capability: secure_development_lifecycle + iso27001_basis: [A.8.25, A.8.27, A.8.28, A.8.29] + cra_target: [Annex_I_Part_I_1, Annex_I_Part_II_3] + relationship: supports + confidence_source: relationship + verification: required + expected_evidence: [secure_development_policy] + rationale: "Secure development exists; the CRA needs demonstrated secure-by-design product properties + regular product security tests. Hence 'supports'." + reviewable_claim: "A secure-development policy does NOT prove CRA secure-by-design product properties." + + - capability: asset_and_configuration_management + iso27001_basis: [A.5.9, A.5.10, A.5.11, A.8.9] + cra_target: [Annex_I_Part_II_1_SBOM] + relationship: partially_supports + confidence_source: relationship + verification: required + expected_evidence: [asset_inventory] + rationale: "Asset/config management UNDERPINS an SBOM but is not one. Hence 'partially_supports'; the SBOM itself is a delta item." + reviewable_claim: "Asset/config management does NOT constitute an SBOM." + +# ── B) DELTA — CRA-specific, NO ISO 27001 analogue. missing for an ISO-only company; ask first. ── +# Each carries why_asked (customer-facing) + dropped_if (what makes the question unnecessary). +delta_requirements: + - capability: sbom_creation + cra_basis: "Annex I, Part II (1) — document components incl. a software bill of materials." + missing_because: "ISO 27001 contains no SBOM requirement." + why_asked: "ISO 27001 does not require a full software bill of materials for every shipped product, so we must check whether one exists." + dropped_if: ["A current machine-readable SBOM (e.g. CycloneDX/SPDX) is produced per product release."] + needed_information: determine_sbom_maturity # never / manual / automatic / continuous + expected_evidence: [sbom] + priority: high + reviewable_claim: "An ISO-27001-only manufacturer typically has no SBOM." + + - capability: security_update_support_period + cra_basis: "Art. 13(8) support period (default >= 5 years) + Annex I Part I (3) / Part II (2,7)." + missing_because: "ISO 27001 defines no product support period or update-provision duty." + why_asked: "The CRA requires a defined security-update support period (default >= 5 years); ISO 27001 says nothing about it." + dropped_if: ["A documented product support/lifecycle policy states the security-update period."] + needed_information: determine_duration + expected_evidence: [support_policy, product_lifecycle_policy] + priority: high + reviewable_claim: "ISO 27001 does not define a product security-update support period." + + - capability: secure_signed_update_distribution + cra_basis: "Annex I, Part II (8) — disseminate updates securely (authenticity/integrity)." + missing_because: "Signed PRODUCT update distribution is product-specific, beyond ISO integrity controls." + why_asked: "The CRA requires updates delivered with authenticity/integrity protection; ISO integrity controls are organizational, not product update signing." + dropped_if: ["Update packages are cryptographically signed and verified on the device (documented)."] + needed_information: verify_existence + expected_evidence: [config_export, test_report] + priority: high + reviewable_claim: "ISO 27001 does not require signed product update distribution." + + - capability: coordinated_vulnerability_disclosure + cra_basis: "Annex I, Part II (5,6) — CVD policy + a reporting contact address." + missing_because: "ISO vulnerability management is internal, not a product-facing CVD/PSIRT." + why_asked: "The CRA requires a way for external parties to report product vulnerabilities (CVD/PSIRT); an internal ISMS process does not provide this." + dropped_if: ["A published CVD policy + reporting contact exists.", "A documented PSIRT procedure is in place."] + needed_information: verify_existence + expected_evidence: [cvd_policy] + priority: high + reviewable_claim: "ISO 27001 does not require a coordinated vulnerability disclosure policy." + + - capability: public_security_advisories + cra_basis: "Annex I, Part II (4,7) — disclose fixed vulnerabilities; provide advisories." + missing_because: "No ISO obligation to publish product security advisories." + why_asked: "The CRA expects fixed vulnerabilities to be disclosed via advisories; ISO 27001 has no such product-facing publication duty." + dropped_if: ["A documented security-advisory process publishes fixed product vulnerabilities."] + needed_information: verify_existence + expected_evidence: [advisory_process] + priority: medium + reviewable_claim: "ISO 27001 does not require public product security advisories." + + - capability: exploited_vuln_and_incident_reporting + cra_basis: "Art. 14 — report actively exploited vulnerabilities + severe incidents to CSIRT/ENISA (24h early warning, 72h notification, final report)." + missing_because: "Statutory authority-reporting with fixed deadlines; no ISO 27001 analogue." + why_asked: "The CRA imposes statutory deadlines (24h/72h) to report exploited vulnerabilities to authorities; ISO 27001 has no such obligation." + dropped_if: ["A documented procedure maps the Art. 14 24h/72h reporting flow to a named responsible role."] + needed_information: verify_existence + expected_evidence: [reporting_procedure] + priority: high + reviewable_claim: "ISO 27001 does not establish CRA Art. 14 authority reporting." + + - capability: secure_by_default_no_default_credentials + cra_basis: "Annex I, Part I (2,4) — secure default configuration; no default passwords." + missing_because: "A product property, not an ISMS control." + why_asked: "The CRA requires the product to ship securely configured with no default passwords; this is a product property the ISMS does not guarantee." + dropped_if: ["A test report confirms secure-by-default config and forced credential change."] + needed_information: verify_existence + expected_evidence: [config_export, test_report] + priority: medium + reviewable_claim: "ISO 27001 does not guarantee secure-by-default product configuration." + + - capability: product_cyber_risk_assessment + cra_basis: "Annex I + technical documentation — cybersecurity risk assessment OF THE PRODUCT." + missing_because: "ISO 27001 risk assessment is organizational, not a product cyber risk assessment." + why_asked: "The CRA technical documentation requires a cybersecurity risk assessment of the PRODUCT; the ISMS risk assessment is organizational." + dropped_if: ["A product-specific cyber risk assessment is part of the technical documentation."] + needed_information: verify_existence + expected_evidence: [product_risk_assessment] + priority: high + reviewable_claim: "An organizational risk assessment is not a CRA product cyber risk assessment." + + - capability: ce_conformity_assessment_and_technical_documentation + cra_basis: "Conformity assessment + CE marking + EU Declaration of Conformity + technical documentation (Annex VII)." + missing_because: "An ISO 27001 certificate is NOT a CRA conformity assessment." + why_asked: "CRA conformity requires a product conformity assessment, CE marking, a Declaration of Conformity and technical documentation; an ISO 27001 certificate does not provide these." + dropped_if: ["A CRA conformity assessment + technical documentation (Annex VII) + DoC exist for the product."] + needed_information: request_evidence + expected_evidence: [technical_documentation, declaration_of_conformity] + priority: high + reviewable_claim: "An ISO 27001 certificate does not satisfy CRA conformity assessment." + +# ── C) Explicit rejections — keep the certificate from being over-read. ── +rejected_assumptions: + - "ISO 27001 does NOT establish a software bill of materials (SBOM)." + - "ISO 27001 does NOT establish a PSIRT or a coordinated vulnerability disclosure process for the product." + - "ISO 27001 does NOT define a product security-update support period." + - "ISO 27001 does NOT produce CRA conformity assessment / CE marking / technical documentation." + - "ISO 27001 does NOT cover Art. 14 reporting of exploited vulnerabilities to CSIRT/ENISA." + - "ISO 27001 does NOT guarantee secure-by-default product configuration or absence of default credentials." + +determinism_goal: > + Two independent ISO 27001 lead auditors reading this pattern should arrive at the SAME set of delta + questions for the same product. If they do not, the pattern is not yet validated. + +review_checklist: + - "Validate every CRA Annex/Article reference with counsel before customer use (indicative here)." + - "Replace capability ids with the Capability Registry MCAP ids once assigned (placeholders here)." + - "Have an ISO 27001 lead auditor confirm the likely_covered vs delta split + each rationale (-> validated)." + - "Confirm each likely_covered item's relationship (supports vs partially_supports) and verification need." diff --git a/backend-compliance/knowledge/transition_patterns/transition_pattern_iso9001_to_cra_v1.yaml b/backend-compliance/knowledge/transition_patterns/transition_pattern_iso9001_to_cra_v1.yaml new file mode 100644 index 00000000..75205806 --- /dev/null +++ b/backend-compliance/knowledge/transition_patterns/transition_pattern_iso9001_to_cra_v1.yaml @@ -0,0 +1,95 @@ +# Transition KNOWLEDGE Pattern (TKP) — ISO 9001 (QMS) -> CRA (Reg. (EU) 2024/2847) +# Curated regulatory KNOWLEDGE. Different character again: ISO 9001 gives PROCESS discipline +# (document/change control, supplier evaluation, release approval) but almost NOTHING product-cyber. +# Therefore the CRA delta is LARGE — the opposite shape of ISMS->TISAX. Used by RTS-002. + +id: TP-ISO9001-CRA-v1 +status: draft # levels: draft(L1) -> reviewed(L2) -> validated(L3, expert) -> proven(L4) +version: 1 + +transition_goal: + from: + standard: "ISO 9001" + edition: "2015" + nature: organizational_qms + to: + regulation: "Cyber Resilience Act" + reference: "Regulation (EU) 2024/2847" + applies_from: "2027-12-11" + nature: product_cybersecurity + one_line: "Move a manufacturer whose only management system is a QMS toward CRA conformity for a product with digital elements." + +provenance: + author: "Claude (Reasoning session) — AI first draft (L1)" + basis: "ISO 9001:2015 (7.5 documented information, 8.4 external providers, 8.5.6 control of changes, 8.6 release) vs CRA Annex I." + reviewed_by: null + reviewed_at: null + validated_by: null + +disclaimer: > + Curated expert knowledge, NOT a normative proof. KEY INSIGHT: ISO 9001 is a QUALITY management + system — it provides process discipline but no information-security or product-cybersecurity + capability. The CRA delta for an ISO-9001-only manufacturer is therefore LARGE (nearly the whole + CRA security + vulnerability-handling + conformity set). Welt-1; confidence from the curated relationship. + +source_state_variants: + certified: "ISO 9001 certified -> the process-discipline assumptions hold; security substance is still missing." + qms_introduced: "QMS implemented but not certified -> downgrade 'partially_supports' to needs_confirmation." + +# ── A) LIKELY COVERED — only process discipline (partially_supports), NOT security. ── +likely_covered: + - capability: document_and_change_control + iso9001_basis: ["7.5", "8.5.6"] + cra_target: [Annex_VII_technical_documentation, Annex_I_Part_II_2_updates] + relationship: partially_supports + confidence_source: relationship + verification: required + expected_evidence: [document_control_procedure] + rationale: "ISO 9001 document/change control gives the discipline to MAINTAIN technical documentation and controlled updates — but no security content. Hence 'partially_supports'." + reviewable_claim: "Document/change control helps maintain CRA technical documentation but does not produce it." + + - capability: supplier_evaluation + iso9001_basis: ["8.4"] + cra_target: [Annex_I_Part_II_1_components] + relationship: partially_supports + confidence_source: relationship + verification: required + expected_evidence: [supplier_evaluation_records] + rationale: "Supplier evaluation gives a basis for supply-chain control — but ISO 9001 evaluates QUALITY, not security/components. Hence 'partially_supports'." + reviewable_claim: "Quality supplier evaluation does not establish component security or an SBOM." + + - capability: release_and_approval_process + iso9001_basis: ["8.6"] + cra_target: [Annex_I_Part_II_8_distribution] + relationship: partially_supports + confidence_source: relationship + verification: required + expected_evidence: [release_procedure] + rationale: "A release/approval gate gives release discipline — but not secure, signed update distribution. Hence 'partially_supports'." + reviewable_claim: "A quality release gate does not establish signed secure update distribution." + +# ── B) DELTA — nearly the whole CRA security + vulnerability-handling + conformity set. ── +delta_requirements: + - {capability: product_cyber_risk_assessment, cra_basis: "Annex I + technical documentation", missing_because: "ISO 9001 has no security risk assessment.", why_asked: "The CRA needs a product cyber risk assessment; a QMS has none.", dropped_if: ["A product cyber risk assessment exists."], needed_information: verify_existence, expected_evidence: [product_risk_assessment], priority: high, reviewable_claim: "ISO 9001 establishes no product cyber risk assessment."} + - {capability: secure_development_lifecycle, cra_basis: "Annex I Part I (1), Part II (3)", missing_because: "ISO 9001 has no secure development.", why_asked: "The CRA requires secure-by-design development + security testing; a QMS does not address security.", dropped_if: ["A documented secure development lifecycle exists."], needed_information: verify_existence, expected_evidence: [secure_development_policy], priority: high, reviewable_claim: "ISO 9001 does not establish a secure development lifecycle."} + - {capability: technical_vulnerability_management, cra_basis: "Annex I Part II (2)", missing_because: "ISO 9001 has no vulnerability management.", why_asked: "The CRA requires handling product vulnerabilities; a QMS has no such process.", dropped_if: ["A vulnerability management process exists."], needed_information: verify_existence, expected_evidence: [vulnerability_management_procedure], priority: high, reviewable_claim: "ISO 9001 does not establish vulnerability management."} + - {capability: coordinated_vulnerability_disclosure, cra_basis: "Annex I Part II (5,6)", missing_because: "No CVD in a QMS.", why_asked: "The CRA requires a way to receive product vulnerability reports (CVD/PSIRT).", dropped_if: ["A published CVD policy + contact exists."], needed_information: verify_existence, expected_evidence: [cvd_policy], priority: high, reviewable_claim: "ISO 9001 does not establish coordinated vulnerability disclosure."} + - {capability: sbom_creation, cra_basis: "Annex I Part II (1)", missing_because: "No SBOM in a QMS.", why_asked: "The CRA requires a software bill of materials.", dropped_if: ["A machine-readable SBOM is produced per release."], needed_information: determine_sbom_maturity, expected_evidence: [sbom], priority: high, reviewable_claim: "ISO 9001 does not produce an SBOM."} + - {capability: security_update_support_period, cra_basis: "Art. 13(8) + Annex I", missing_because: "No support period in a QMS.", why_asked: "The CRA requires a defined security-update support period.", dropped_if: ["A product support/lifecycle policy defines the period."], needed_information: determine_duration, expected_evidence: [support_policy], priority: high, reviewable_claim: "ISO 9001 does not define a security-update support period."} + - {capability: secure_signed_update_distribution, cra_basis: "Annex I Part II (8)", missing_because: "No signed updates in a QMS.", why_asked: "The CRA requires authenticity/integrity-protected updates.", dropped_if: ["Updates are cryptographically signed and verified."], needed_information: verify_existence, expected_evidence: [config_export], priority: high, reviewable_claim: "ISO 9001 does not establish signed update distribution."} + - {capability: exploited_vuln_and_incident_reporting, cra_basis: "Art. 14", missing_because: "No Art. 14 reporting in a QMS.", why_asked: "The CRA imposes 24h/72h reporting of exploited vulnerabilities to authorities.", dropped_if: ["A documented Art. 14 reporting procedure exists."], needed_information: verify_existence, expected_evidence: [reporting_procedure], priority: high, reviewable_claim: "ISO 9001 does not establish CRA Art. 14 reporting."} + - {capability: access_control_and_authentication, cra_basis: "Annex I Part I (4)", missing_because: "No security access control in a QMS.", why_asked: "The CRA requires product authentication / no default credentials.", dropped_if: ["Product authentication is documented + tested."], needed_information: verify_existence, expected_evidence: [access_control_policy], priority: medium, reviewable_claim: "ISO 9001 does not establish product authentication."} + - {capability: ce_conformity_assessment_and_technical_documentation, cra_basis: "Annex VII + CE", missing_because: "No CRA conformity in a QMS.", why_asked: "CRA conformity needs a product conformity assessment + CE + technical documentation + DoC.", dropped_if: ["A CRA conformity assessment + technical documentation exist."], needed_information: request_evidence, expected_evidence: [technical_documentation, declaration_of_conformity], priority: high, reviewable_claim: "ISO 9001 does not satisfy CRA conformity assessment."} + +rejected_assumptions: + - "ISO 9001 does NOT establish information security or product cybersecurity." + - "ISO 9001 does NOT establish secure development, vulnerability management, SBOM, CVD, or Art. 14 reporting." + - "An ISO 9001 certificate does NOT satisfy CRA conformity assessment." + +determinism_goal: > + Two independent auditors should agree that an ISO-9001-only manufacturer needs almost the whole CRA + security set; the QMS reduces effort only on documentation/change/release discipline. + +review_checklist: + - "Confirm the (deliberately large) delta with a CRA expert." + - "Replace capability ids with Capability Registry MCAP ids once assigned." diff --git a/backend-compliance/knowledge/vocabulary/journey_classes.yaml b/backend-compliance/knowledge/vocabulary/journey_classes.yaml new file mode 100644 index 00000000..557c5377 --- /dev/null +++ b/backend-compliance/knowledge/vocabulary/journey_classes.yaml @@ -0,0 +1,30 @@ +# Domain Vocabulary — Journey CLASSES (PROVISIONAL). A class clusters journey instances that are +# "the same reise". So we do NOT write a new journey for every certification when many share a class. +# PROVISIONAL: Journey Class is a NEW abstraction -> its OWN Rule of Three (>= 3 instances per class +# before minting MJRN ids). Endpoints reference regulation vocabulary ids (see regulations.yaml). + +id: VOCAB-journey-classes-v1 +status: provisional +classes: + - id: infosec-to-product-cyber # provisional id, NOT a minted MJRN + name: "Information Security → Product Cybersecurity" + from_kind: information_security + to_kind: product_cybersecurity + instances: + - {from: iso27001, to: cra} # ✅ modelled (TP-ISO27001-CRA-v1) + - {from: tisax, to: cra} # ⏳ Rule-of-Three transition #3 + - {from: iec62443, to: cra} # ⏳ + + - id: qm-to-product-compliance + name: "Quality Management → Product Compliance/Safety" + from_kind: quality_management + to_kind: product_compliance_safety + instances: + - {from: iso9001, to: cra} # ✅ modelled (TP-ISO9001-CRA-v1) + - {from: iso9001, to: maschinenvo} # ⏳ Rule-of-Three transition #2 — INSTANCE of this class, not a new kind + - {from: iso13485, to: mdr} # same CLASS, different domain (medical) — proves the class generalises + +note: > + Befund: ISO9001→MaschinenVO ist KEINE neue Journey-Art, sondern eine INSTANZ der Klasse + „Quality Management → Product Compliance/Safety" (wie ISO9001→CRA, ISO13485→MDR). Das ist genau die + Duplikation, die das Vokabular verhindert. diff --git a/backend-compliance/knowledge/vocabulary/regulations.yaml b/backend-compliance/knowledge/vocabulary/regulations.yaml new file mode 100644 index 00000000..556651c6 --- /dev/null +++ b/backend-compliance/knowledge/vocabulary/regulations.yaml @@ -0,0 +1,21 @@ +# Domain Vocabulary — regulation/standard IDENTITIES (Requirement Sources + Targets). +# Each has a stable id + a canonical name + every alias/spelling. SOLVES the regulation-ID +# normalization that the Transition Coverage KPI + Knowledge Intake flagged (CRA vs "Cyber Resilience +# Act"). Reasoning seeds this; @Legal-KG / @Execution please adopt as the SHARED vocabulary. +# Not runtime, no minting — a shared knowledge artifact. + +id: VOCAB-regulations-v1 +regulations: + - {id: cra, canonical: "Cyber Resilience Act", aliases: [CRA, "Cyber Resilience Act", "Regulation (EU) 2024/2847"]} + - {id: maschinenvo, canonical: "Maschinenverordnung", aliases: [MaschinenVO, Maschinenverordnung, "Machinery Regulation", "Regulation (EU) 2023/1230"]} + - {id: iso9001, canonical: "ISO 9001", aliases: [ISO9001, "ISO 9001", "ISO/IEC 9001", QMS, "Quality Management System"]} + - {id: iso27001, canonical: "ISO/IEC 27001", aliases: [ISO27001, "ISO 27001", "ISO/IEC 27001", ISMS, "Information Security Management System"]} + - {id: tisax, canonical: "TISAX", aliases: [TISAX, "Trusted Information Security Assessment Exchange"]} + - {id: iec62443, canonical: "IEC 62443", aliases: [IEC62443, "IEC 62443", "ISO/IEC 62443"]} + - {id: nis2, canonical: "NIS2", aliases: [NIS2, "NIS 2", "Directive (EU) 2022/2555"]} + - {id: dataact, canonical: "Data Act", aliases: [DataAct, "Data Act", "Regulation (EU) 2023/2854"]} + - {id: iso13485, canonical: "ISO 13485", aliases: [ISO13485, "ISO 13485"]} + - {id: mdr, canonical: "MDR", aliases: [MDR, "Medical Device Regulation", "Regulation (EU) 2017/745"]} + - {id: iec62304, canonical: "IEC 62304", aliases: [IEC62304, "IEC 62304"]} + - {id: iso14001, canonical: "ISO 14001", aliases: [ISO14001, "ISO 14001"]} + - {id: iatf16949, canonical: "IATF 16949", aliases: [IATF16949, "IATF 16949", IATF]} diff --git a/backend-compliance/reference_scenarios/_helpers.py b/backend-compliance/reference_scenarios/_helpers.py new file mode 100644 index 00000000..f7847660 --- /dev/null +++ b/backend-compliance/reference_scenarios/_helpers.py @@ -0,0 +1,281 @@ +# ruff: noqa +# mypy: ignore-errors +"""Rendering helpers for the Reference Scenario Suite generator. + +Holds the shared mutable output buffers (OUT, ROLLUP) and the small markdown helpers so the +generator script (`generate.py`) stays under the LOC budget. Not product code; not imported by +the app — only by the generator (run via `PYTHONPATH=. python3 reference_scenarios/generate.py`). +""" +from __future__ import annotations + +from typing import List, Tuple + +Row = Tuple[str, str, str] +OUT: List[str] = [] +ROLLUP: List[str] = [] + + +def w(s: str = "") -> None: + OUT.append(s) + + +def coverage_table(rows: List[Row]) -> None: + w("**Architecture Coverage**") + w("") + w("| Layer | Status | Hinweis |") + w("|---|---|---|") + for layer, status, note in rows: + w("| %s | **%s** | %s |" % (layer, status, note)) + ROLLUP.append(status) + w("") + + +def reg_map_block(rmap) -> None: + w("**Expected Regulatory Map**") + w("") + w("> " + rmap.executive_summary) + w("") + for v in rmap.applicable_regulations: + obs = ", ".join(o.obligation_id for o in v.obligations) or v.obligations_note + w("- **%s** (%s) — Pflichten: %s" % (v.regulation_id, v.name, obs)) + for u in rmap.uncertain_regulations: + w("- _unsicher_ %s — fehlt: %s" % (u.regulation_id, ", ".join(u.missing_facts) or "-")) + for ov in rmap.overlaps: + w("- Overlap %s: %s" % (ov.overlap_group_id, ", ".join(ov.shared_obligations))) + for ev, ids in rmap.shared_evidence.items(): + w("- 1 Nachweis `%s` => %d Pflichten" % (ev, len(ids))) + w("") + + +def unsupported_block(rmap) -> None: + w("**Expected Unsupported Domains**") + w("") + if not rmap.unsupported_domains: + w("- keine — alle getriggerten Domaenen sind im Korpus") + for d in rmap.unsupported_domains: + w("- `%s` (Trigger: %s) -> %s" % (d.domain, d.trigger, d.note)) + w("") + + +def interp_status(verdict_value: str) -> str: + return "PARTIAL" if verdict_value in ("uncertain", "unsupported") else "PASS" + + +def knowledge_intake_section(base_dir) -> None: + """Render the Knowledge Intake section (kept here so generate.py stays under the LOC budget).""" + import os + import yaml + from compliance.knowledge_intake import ( + DocumentDescriptor, assess_document_impact, build_knowledge_index, + ) + + def _load(sub): + d = os.path.join(base_dir, "..", "knowledge", sub) + return [yaml.safe_load(open(os.path.join(d, f), encoding="utf-8")) + for f in sorted(os.listdir(d)) if f.endswith(".yaml")] + + idx = build_knowledge_index( + _load("transition_patterns"), _load("implementation_playbooks"), + _load("reference_transition_scenarios"), obligation_index={"CRA": ["cra_obl_1", "cra_obl_2"]}) + docs = [ + DocumentDescriptor(document_id="ENISA CRA SBOM-FAQ", regulations=["CRA"], keywords=["sbom", "vulnerability"], document_type="faq"), + DocumentDescriptor(document_id="EU Umwelt-Leitfaden", regulations=["UmweltVO"], keywords=["wastewater"], document_type="guidance"), + DocumentDescriptor(document_id="Marketing-Blog", keywords=["newsletter"], document_type="blog"), + ] + w("## Knowledge Intake — Impact zuerst, Extraktion später") + w("") + w('_Vor dem Parser: ein neues Dokument NUR einordnen und seinen Impact auf den bestehenden Wissensbestand bestimmen. „Von N Dokumenten verändern wenige tatsächlich unser Wissen." Deterministisch, keine Extraktion, kein LLM._') + w("") + w("| Dokument | Impact | betrifft | Empfehlung |") + w("|---|---|---|---|") + for d in docs: + kp = assess_document_impact(d, idx) + touch = "neue Domäne" if kp.new_domain else "%dC·%dPB·%dRTS·%dObl" % ( + len(kp.affected_capabilities), len(kp.affected_playbooks), + len(kp.affected_reference_scenarios), len(kp.affected_obligations)) + w("| %s | **%s** | %s | %s |" % (d.document_id, kp.impact_level.value, touch, kp.recommendation.split(" —")[0])) + w("") + w("**Beispiel-Knowledge-Package** (`%s`): %s" % (docs[0].document_id, assess_document_impact(docs[0], idx).impact_summary)) + w("") + w('_So entsteht bei jedem neuen Dokument eine Impact-Analyse statt „200 Seiten PDF" — Targeted Updating statt Schreiben._') + w("") + coverage_table([ + ("Knowledge Intake (Klassifikation+Impact)", "PASS", "%d Regelwerke / %d Capabilities im Index" % (len(idx.regulations), len(idx.capability_regulations))), + ("Impact-Triage (HIGH/LOW/NONE/new_domain)", "PASS", "3 Beispiel-Dokumente korrekt eingeordnet"), + ("Regelwerk-ID-Normalisierung", "TODO", "CRA vs Cyber Resilience Act vereinheitlichen"), + ]) + + +def completeness_section() -> None: + """Render the Regulatory Completeness section (kept here so generate.py stays under the LOC budget).""" + from compliance.completeness import assess_completeness + + rep = assess_completeness( + identified_regulations=["CRA", "MaschinenVO", "EMV", "Environmental", "DataAct"], + corpus_status={"CRA": "validated", "MaschinenVO": "validated", "EMV": "unsupported", + "Environmental": "unsupported", "DataAct": "validated"}, + uncertain=[{"regulation": "DataAct", "deciding_question": "generates_usage_data", "reason": "generates_usage_data = unbekannt"}], + assumptions=[{"key": "Funkmodul", "value": "nein"}, {"key": "personenbezogene Nutzungsdaten", "value": "nein"}], + assessed_obligations=128) + w("## Regulatory Completeness — was wir bewerten konnten, und was bewusst nicht") + w("") + w('_Interne Qualitätsmaschine (KEIN Confidence-Score): trennt IDENTIFIZIERT von BEWERTET und begründet jede Lücke. Keine Prozentzahl — auditierbar und ehrlich: „Wir zeigen auch, was wir noch nicht wissen und warum."_') + w("") + w("**%s**" % rep.completeness_summary) + w("") + w("> %s" % rep.audit_statement) + w("") + w("- **Bewertet:** %s (%d Pflichten)" % (", ".join(rep.assessed_regulations), rep.assessed_obligations)) + w("- **Offen (jeweils begründet):**") + for e in rep.exclusions: + dq = (" → Rückfrage: `%s`" % e.deciding_question) if e.deciding_question else "" + w(" - `%s` — %s `[%s]`%s" % (e.subject, e.reason, e.resolution, dq)) + w("- **Annahmen:** %s" % ", ".join("%s=%s" % (a.key, a.value) for a in rep.assumptions)) + w("") + w("_Sobald der Umwelt-Korpus (ISO 14001 etc.) landet, kippt `Environmental` automatisch von offen auf bewertet — die Completeness Engine dokumentiert den Fortschritt je Domäne._") + w("") + coverage_table([ + ("Regulatory Completeness (auditierbar)", "PASS", rep.completeness_summary), + ("Begründete Ausschlüsse (Korpus/Anwendbarkeit)", "PASS", "%d Ausschlüsse, alle mit Grund" % len(rep.exclusions)), + ("Fortschritts-Doku je Domäne", "PASS", "Environmental offen→validated bei Korpus-Landung"), + ]) + + +def _regulation_aliases(base_dir): + """Build a normalized alias -> canonical-id map from the Domain Vocabulary (regulations.yaml).""" + import os + import yaml + path = os.path.join(base_dir, "..", "knowledge", "vocabulary", "regulations.yaml") + amap = {} + with open(path, encoding="utf-8") as h: + for r in (yaml.safe_load(h) or {}).get("regulations", []): + for name in [r["canonical"]] + list(r.get("aliases", [])): + amap["".join(c for c in str(name).lower() if c.isalnum())] = r["id"] + return amap + + +def _canon_reg(s, amap): + """Canonicalize a regulation string via the vocabulary (replaces the old hard-coded alias maps).""" + return amap.get("".join(c for c in str(s).lower() if c.isalnum()), + "".join(c for c in str(s).lower() if c.isalnum())) + + +def domain_programs_section(base_dir) -> None: + """Domain Knowledge Program v1 — per-domain maturity KPI DERIVED from the corpus (computed-not-stored).""" + import os + import yaml + from compliance.knowledge_intake import build_knowledge_index + + def _load(sub): + d = os.path.join(base_dir, "..", "knowledge", sub) + return [yaml.safe_load(open(os.path.join(d, f), encoding="utf-8")) + for f in sorted(os.listdir(d)) if f.endswith(".yaml")] + + idx = build_knowledge_index(_load("transition_patterns"), _load("implementation_playbooks"), + _load("reference_transition_scenarios")) + pdir = os.path.join(base_dir, "..", "knowledge", "programs") + _all = [yaml.safe_load(open(os.path.join(pdir, f), encoding="utf-8")) + for f in sorted(os.listdir(pdir)) if f.endswith(".yaml")] + progs = sorted((p for p in _all if "backlog_rank" in p), key=lambda p: p["backlog_rank"]) # domain programs only + + _amap = _regulation_aliases(base_dir) # Domain Vocabulary (regulations.yaml) + + def _canon(r): + return _canon_reg(r, _amap) + + def _hits(reg_lists, src): + cs = {_canon(s) for s in src} + return [k for k, regs in reg_lists.items() if cs & {_canon(x) for x in regs}] + + def _source_modeled(index, source, canon): + c = canon(source) + in_tp = any(c in {canon(x) for x in regs} for regs in index.transition_patterns.values()) + in_rts = any(c in {canon(x) for x in regs} for regs in index.reference_scenarios.values()) + in_pb = any(c in {canon(x) for x in index.capability_regulations.get(cap, [])} for cap in index.playbook_capabilities) + return in_tp or in_rts or in_pb + + w("## Domain Knowledge Program v1 — Reifegrad je Domäne (reproduzierbarer KPI)") + w("") + w('_Engpass = Domänenmodellierung. Jede Domäne läuft durch DIESELBE 7-Stufen-Produktionsstraße (Domain Model → Requirement Sources → Capability Registry → Transition Patterns → Playbooks → Reference Scenarios → Completeness). Reifegrad aus dem ECHTEN Korpus abgeleitet (computed-not-stored), keine Marketingzahl. Einstieg über Industry, nicht Regelwerk._') + w("") + w("| Rank | Domäne | Reifegrad (Sources modelliert) | modelliert/total | Korpus TP·PB·RTS |") + w("|---|---|---|---|---|") + for p in progs: + src = p.get("typical_requirement_sources", []) + tp, rts = _hits(idx.transition_patterns, src), _hits(idx.reference_scenarios, src) + cs = {_canon(s) for s in src} + pb = [c for c in idx.playbook_capabilities if cs & {_canon(x) for x in idx.capability_regulations.get(c, [])}] + modeled = [s for s in src if _source_modeled(idx, s, _canon)] # sources with >=1 corpus artifact + breadth = (len(modeled) / len(src)) if src else 0.0 # honest differentiator (not CRA-shared depth) + filled = int(round(breadth * 10)) + w("| %d | **%s** | `%s` %d%% | %d/%d | %d·%d·%d |" % ( + p.get("backlog_rank", 99), p["name"], "█" * filled + "░" * (10 - filled), + int(round(breadth * 100)), len(modeled), len(src), len(tp), len(pb), len(rts))) + w("") + w('_Industry-Einstieg + ETO-Hypothese: jede Domäne kennt ihre typischen Sources + Zertifikate → vor dem Onboarding „diese Prozesswelt ist wahrscheinlich vorhanden" (Hypothese, nie Wahrheit; speist Company 2A als `inferred`). Backlog nach Kundennutzen, KPI nach echtem Korpusstand — beides bewusst getrennt._') + w("") + coverage_table([ + ("Domain Knowledge Program (7-Stufen-Produktionsstraße)", "PASS", "%d Domänen im Backlog, Industrial Automation #1" % len(progs)), + ("Reifegrad-KPI (computed-not-stored)", "PASS", "aus echtem Korpus abgeleitet (TP/PB/RTS je Domäne)"), + ("Regelwerk-ID-Normalisierung (Domain Vocabulary)", "PASS", "Aliase aus `vocabulary/regulations.yaml`, nicht mehr hartkodiert"), + ]) + + +def transition_coverage_section(base_dir) -> None: + """Transition Coverage — the TRANSITION is the unit of knowledge; status DERIVED from the corpus.""" + import os + import yaml + + kdir = os.path.join(base_dir, "..", "knowledge") + with open(os.path.join(kdir, "programs", "transitions.yaml"), encoding="utf-8") as h: + backlog = yaml.safe_load(h) + tpdir = os.path.join(kdir, "transition_patterns") + pats = [] + for f in sorted(os.listdir(tpdir)): + if not f.endswith(".yaml"): + continue + d = yaml.safe_load(open(os.path.join(tpdir, f), encoding="utf-8")) + goal = d.get("transition_goal", {}) + frm = goal.get("from", {}).get("standard", "") + to = goal.get("to") + tos = [it.get("regulation") or it.get("framework") or it.get("target") + for it in (to if isinstance(to, list) else [to]) if isinstance(it, dict)] + pats.append((frm, [t for t in tos if t], str(d.get("status", "draft")))) + + _amap = _regulation_aliases(base_dir) # Domain Vocabulary (regulations.yaml) + + def _c(s): + return _canon_reg(s, _amap) + + _RANK = {"draft": 1, "reviewed": 2, "validated": 3, "proven": 4} + _ICON = {0: "⚪ nicht begonnen", 1: "🟡 Draft", 2: "✅ reviewed", 3: "✅ validated", 4: "✅ Gold"} + + def _status(frm, to): + best = 0 + for pf, ptos, st in pats: + if _c(pf) == _c(frm) and _c(to) in {_c(x) for x in ptos}: + best = max(best, _RANK.get(st, 1)) + return best + + w("## Transition Coverage — die Transition ist die Wissenseinheit (Operational Knowledge)") + w("") + w('_Der Kunde kauft nicht „EMV-Domain", sondern „wir haben ISO 9001 — helfen Sie uns beim CRA". Die Wissenseinheit ist die TRANSITION (nicht das Gesetz). Status je Transition aus dem echten Pattern-Korpus abgeleitet (computed-not-stored). Drei Ebenen: Regulatory → **Operational (hier, größter Differenzierer)** → Verification (Vision V2)._') + w("") + w("| Prio | Transition | Status |") + w("|---|---|---|") + rows = sorted(backlog["transitions"], key=lambda t: -t["priority"]) + done = 0 + for t in rows: + s = _status(t["from"], t["to"]) + done += 1 if s else 0 + w("| %s | `%s → %s` | %s |" % ("⭐" * t["priority"], t["from"], t["to"], _ICON[s])) + w("") + gaps = [t for t in rows if _status(t["from"], t["to"]) == 0] + if gaps: + w('**Größte Lücke (Track B als Nächstes):** `%s → %s` (%s) — höchstnachgefragte Transition OHNE Pattern. Stärkerer Produktindikator als „EMV 30%% modelliert".' % (gaps[0]["from"], gaps[0]["to"], "⭐" * gaps[0]["priority"])) + w("") + coverage_table([ + ("Transition Coverage (Operational Knowledge)", "PASS", "%d von %d Top-Transitionen mit Pattern" % (done, len(rows))), + ("Wissenseinheit = Transition (nicht Gesetz)", "PASS", "verkauft wird der Übergang, z. B. ISO9001→CRA"), + ("3 Ebenen Regulatory→Operational→Verification", "PASS", "Operational = größter Differenzierer (ADR-010)"), + ]) diff --git a/backend-compliance/reference_scenarios/architecture_stability_kpi.md b/backend-compliance/reference_scenarios/architecture_stability_kpi.md new file mode 100644 index 00000000..098f24b0 --- /dev/null +++ b/backend-compliance/reference_scenarios/architecture_stability_kpi.md @@ -0,0 +1,40 @@ +# Architecture Stability + Knowledge Velocity — Phase Ω (Evidence of Generality) + +_Der Fokus hat sich verschoben: nicht mehr „kann die Architektur das?", sondern „wo versagt sie bei echtem Fachwissen?". Diese zwei KPIs erhebt kaum jemand. Eine neue Domäne ist eine ZEILE im Ledger (Daten), nie eine Codeänderung — genau das macht den KPI auditierbar._ + +## Architecture Stability — pro Quelle: neue Runtime-Klassen? neue Pipeline? neue Capability-Typen? + +| Quelle | Familie | neue Runtime-Klassen | neue Pipeline | neue Capability-Typen | Ergebnis | +|---|---|---:|---:|---:|---| +| Cyber Resilience Act (CRA) | cyber | 0 | 0 | 13 | ✅ | +| Maschinenverordnung (MaschinenVO) | cyber | 0 | 0 | 4 | ✅ | +| TISAX | cyber | 0 | 0 | 5 | ✅ | +| Public Tender (öffentliche Ausschreibung) | cyber | 0 | 0 | 3 | ✅ | +| OEM Specification (Lastenheft) | cyber | 0 | 0 | 4 | ✅ | +| ISO 14001 -> Environmental/Material (REACH/RoHS/Batterie/Wasser/Energie/Abfall) | non_cyber | 0 | 0 | 16 | ✅ | +| Automotive ECU for OEM X (CRA / UNECE R155+R156 / IATF 16949 / TISAX / ASPICE / OEM spec) | cyber | 0 | 0 | 14 | ✅ | +| ISO 13485 -> Medical device (MDR / IEC 62304 / ISO 14971 / IEC 81001-5-1) | non_cyber | 0 | 0 | 7 | ✅ | + +- **Architecture Stability: 8/8 = 100%** der Quellen ohne neue Runtime-Klasse und ohne neue Pipeline. +- **Knowledge Velocity: 8/8 = 100%** der Quellen **data-only** integriert (kein Entwickler nötig). +- **Generalität über Cyber hinaus: 2/8 Quellen NICHT-Cyber** (Umwelt) — trugen die Pipeline ebenfalls 0/0. Das ist der eigentliche Test (ein anderes Denkmodell, nicht noch ein Cyber-Regelwerk). +- **Capability-Modell-Frühindikator: 66 neue Typen gesamt, Maximum 16** (Umwelt, erste Nicht-Cyber-Domäne) — in Range, KEIN Granularitätsalarm (Alarm ≈ eine Domäne braucht plötzlich ~80 neue Typen bei 0 Runtime-Change → Modell zu grob/fein). + +## Ehrlichkeit: die Pipeline-Funktionen sind EINMALIG (jetzt eingefroren) +- 6 domänen-AGNOSTISCHE Funktionen, einmal gebaut, nicht je Domäne: `transition_reasoning (RS-005)`, `optimization`, `journey_matcher (ADR-011)`, `playbook`, `completeness`, `company (2A)`. +- Die letzte (`journey_matcher`) war der **letzte architektonische Baustein** (ADR-011). Ab hier: Wissensarbeit, nicht Architektur. + +## Drei saubere Wissensebenen (greifen ineinander, vermischen sich nicht) +| Ebene | Inhalt | +|---|---| +| **Beschreibend** (was IST) | Requirements, Capabilities, Evidence | +| **Transformation** (wie BEWEGEN) | Delta, Journey, Roadmap | +| **Produktion** (wie TUN/BEWEISEN) | Playbooks, Verification, Reference Scenarios | + +## Die drei Erfolgsfragen ab jetzt (statt Coverage) +1. **Musste für eine neue Domäne Runtime-Code geändert werden?** → bisher: **nein** (8/8). +2. **Knowledge Velocity** — neues Wissen ohne Entwickler aufnehmbar? → bisher: **ja** (8/8 data-only). +3. **Architecture Stability** — bestehende Capability/Journey strukturell ändern oder nur Daten ergänzen? → bisher: **nur Daten**. + +> **Befund:** Über fünf Zielarten und sechs Quellen blieb `Reality → Evidence → Capability → Required → Delta → Journey → Roadmap → Playbooks → Verification` unverändert. Das ist der eigentliche Nachweis: keine Compliance-Architektur, sondern eine allgemeine Requirements-Verifikationsarchitektur, die ihre Generalität UNTER realer fachlicher Belastung behält. Der nächste Test ist nicht ein Feature, sondern die nächste echte Domäne (Umwelt-Cluster · Automotive · Medizintechnik · Payment) — jede als neue Ledger-Zeile, bei stabilem KPI. + diff --git a/backend-compliance/reference_scenarios/architecture_stability_kpi.py b/backend-compliance/reference_scenarios/architecture_stability_kpi.py new file mode 100644 index 00000000..bf17c0ca --- /dev/null +++ b/backend-compliance/reference_scenarios/architecture_stability_kpi.py @@ -0,0 +1,90 @@ +# ruff: noqa +# mypy: ignore-errors +"""Architecture Stability + Knowledge Velocity KPI — Phase Ω (Evidence of Generality). + +Not "can the architecture do this?" but "where does it fail under real domain knowledge?". This reads +the integration ledger and computes two KPIs almost nobody measures: + - Architecture Stability : share of integrated Requirement Sources that needed 0 new runtime classes + AND no new pipeline. + - Knowledge Velocity : share of sources a DOMAIN EXPERT could integrate data-only (no developer). + +A new domain is a ROW in the ledger (data), never a code change — so this KPI literally improves by +adding data, which is the proof. Non-runtime -> no deploy. +Run: cd backend-compliance && PYTHONPATH=. python3 reference_scenarios/architecture_stability_kpi.py +""" +from __future__ import annotations + +import os +import yaml + +OUT = [] + + +def w(s=""): + OUT.append(s) + + +_LEDGER = os.path.join(os.path.dirname(__file__), "..", "knowledge", "architecture_stability", "integration_ledger.yaml") +L = yaml.safe_load(open(_LEDGER, encoding="utf-8")) +sources = L["sources"] +n = len(sources) + +stable = [s for s in sources if s["new_runtime_classes"] == 0 and not s["new_pipeline"]] +data_only = [s for s in sources if s["integration_kind"] == "data_only"] +arch_stability = len(stable) / n if n else 0.0 +knowledge_velocity = len(data_only) / n if n else 0.0 + +w("# Architecture Stability + Knowledge Velocity — Phase Ω (Evidence of Generality)") +w("") +w('_Der Fokus hat sich verschoben: nicht mehr „kann die Architektur das?", sondern „wo versagt sie bei echtem Fachwissen?". Diese zwei KPIs erhebt kaum jemand. Eine neue Domäne ist eine ZEILE im Ledger (Daten), nie eine Codeänderung — genau das macht den KPI auditierbar._') +w("") +w("## Architecture Stability — pro Quelle: neue Runtime-Klassen? neue Pipeline? neue Capability-Typen?") +w("") +w("| Quelle | Familie | neue Runtime-Klassen | neue Pipeline | neue Capability-Typen | Ergebnis |") +w("|---|---|---:|---:|---:|---|") +for s in sources: + ok = "✅" if (s["new_runtime_classes"] == 0 and not s["new_pipeline"]) else "❌" + w("| %s | %s | %d | %s | %d | %s |" % ( + s["source"], s.get("family", "-"), s["new_runtime_classes"], + "ja" if s["new_pipeline"] else "0", s.get("new_capability_types", 0), ok)) +w("") +non_cyber = [s for s in sources if s.get("family") == "non_cyber"] +total_types = sum(s.get("new_capability_types", 0) for s in sources) +max_types = max((s.get("new_capability_types", 0) for s in sources), default=0) +w("- **Architecture Stability: %d/%d = %d%%** der Quellen ohne neue Runtime-Klasse und ohne neue Pipeline." % ( + len(stable), n, round(arch_stability * 100))) +w("- **Knowledge Velocity: %d/%d = %d%%** der Quellen **data-only** integriert (kein Entwickler nötig)." % ( + len(data_only), n, round(knowledge_velocity * 100))) +w("- **Generalität über Cyber hinaus: %d/%d Quellen NICHT-Cyber** (Umwelt) — trugen die Pipeline ebenfalls 0/0. Das ist der eigentliche Test (ein anderes Denkmodell, nicht noch ein Cyber-Regelwerk)." % ( + len(non_cyber), n)) +w("- **Capability-Modell-Frühindikator: %d neue Typen gesamt, Maximum %d** (Umwelt, erste Nicht-Cyber-Domäne) — in Range, KEIN Granularitätsalarm (Alarm ≈ eine Domäne braucht plötzlich ~80 neue Typen bei 0 Runtime-Change → Modell zu grob/fein)." % ( + total_types, max_types)) +w("") + +# pipeline functions = one-time, domain-agnostic infrastructure (honesty: not per-domain costs) +pf = L["pipeline_functions"] +w("## Ehrlichkeit: die Pipeline-Funktionen sind EINMALIG (jetzt eingefroren)") +w("- %d domänen-AGNOSTISCHE Funktionen, einmal gebaut, nicht je Domäne: %s." % ( + len(pf), ", ".join("`%s`" % f["fn"] for f in pf))) +w("- Die letzte (`journey_matcher`) war der **letzte architektonische Baustein** (ADR-011). Ab hier: Wissensarbeit, nicht Architektur.") +w("") + +# three knowledge layers (the architecture has settled symmetrically) +kl = L["knowledge_layers"] +w("## Drei saubere Wissensebenen (greifen ineinander, vermischen sich nicht)") +w("| Ebene | Inhalt |") +w("|---|---|") +w("| **Beschreibend** (was IST) | %s |" % ", ".join(kl["descriptive"])) +w("| **Transformation** (wie BEWEGEN) | %s |" % ", ".join(kl["transformation"])) +w("| **Produktion** (wie TUN/BEWEISEN) | %s |" % ", ".join(kl["production"])) +w("") + +w("## Die drei Erfolgsfragen ab jetzt (statt Coverage)") +w("1. **Musste für eine neue Domäne Runtime-Code geändert werden?** → bisher: **nein** (%d/%d)." % (len(stable), n)) +w("2. **Knowledge Velocity** — neues Wissen ohne Entwickler aufnehmbar? → bisher: **ja** (%d/%d data-only)." % (len(data_only), n)) +w("3. **Architecture Stability** — bestehende Capability/Journey strukturell ändern oder nur Daten ergänzen? → bisher: **nur Daten**.") +w("") +w('> **Befund:** Über fünf Zielarten und sechs Quellen blieb `Reality → Evidence → Capability → Required → Delta → Journey → Roadmap → Playbooks → Verification` unverändert. Das ist der eigentliche Nachweis: keine Compliance-Architektur, sondern eine allgemeine Requirements-Verifikationsarchitektur, die ihre Generalität UNTER realer fachlicher Belastung behält. Der nächste Test ist nicht ein Feature, sondern die nächste echte Domäne (Umwelt-Cluster · Automotive · Medizintechnik · Payment) — jede als neue Ledger-Zeile, bei stabilem KPI.') +w("") + +print("\n".join(OUT)) diff --git a/backend-compliance/reference_scenarios/automotive_convergence_stress_test.md b/backend-compliance/reference_scenarios/automotive_convergence_stress_test.md new file mode 100644 index 00000000..a035f0fa --- /dev/null +++ b/backend-compliance/reference_scenarios/automotive_convergence_stress_test.md @@ -0,0 +1,36 @@ +# Automotive Convergence Stress Test — überlebt EINE Capability viele Quellen? (Phase Ω #2) + +_Nicht noch eine Domäne, sondern eine andere Eigenschaft: bleibt das Modell stabil, wenn dieselbe Capability gleichzeitig aus vielen überlappenden Quellen gespeist wird? Realistisch: ein Zulieferer (ISO 9001 + IATF 16949 + TISAX + ASPICE + CSMS + SUMS) entwickelt ein Steuergerät für OEM X. Sieben Quellen, bewusste Überlappung. Data-only, keine echten Namen._ + +## 1. Sieben überlappende Quellen, eine Engine +- Quellen: `CRA`(regulation), `UNECE_R155_CSMS`(regulation), `UNECE_R156_SUMS`(regulation), `IATF_16949`(certification), `TISAX`(certification), `ASPICE`(process), `OEM_X_Spec`(contract). +- **27 distinct geforderte Capabilities** für „Steuergerät → OEM X" — über dieselbe `assess_transition`-Engine, 0 neue Runtime-Klassen. +- Delta (fehlt dem Profil): **7** Capabilities. + +## 2. Capability Convergence — dieselbe Capability aus vielen Quellen +| Capability | Sources | Distinct Source Types | aus | +|---|---:|---:|---| +| `technical_vulnerability_management` | 4 | 3 | CRA, OEM_X_Spec, TISAX, UNECE_R155_CSMS | +| `secure_signed_update_distribution` | 4 | 2 | CRA, OEM_X_Spec, UNECE_R155_CSMS, UNECE_R156_SUMS | +| `access_control_and_authentication` | 3 | 2 | CRA, TISAX, UNECE_R155_CSMS | +| `incident_management` | 3 | 2 | CRA, TISAX, UNECE_R155_CSMS | +| `identify_software_versions_rxswin` | 2 | 2 | OEM_X_Spec, UNECE_R156_SUMS | +| `protect_prototypes` | 2 | 2 | OEM_X_Spec, TISAX | +| `supplier_security` | 2 | 2 | OEM_X_Spec, TISAX | +| `product_cyber_risk_assessment` | 2 | 1 | CRA, UNECE_R155_CSMS | + +→ **`technical_vulnerability_management`** ist am konvergentesten: **4 Quellen** über **3 Quelltypen** — eine Maßnahme, viele Nachweiswelten. Genau hier entsteht der wirtschaftliche Nutzen (nicht „300 Gesetze", sondern „eine Capability ersetzt fünf Nachweiswelten"). + +## 3. Existing-vs-New — beginnt die Registry zu konvergieren? +- **Bereits vorhandene MCAPs (Reuse aus Cyber/Umwelt): 13/27 = 48%** — z. B. `access_control_and_authentication`, `coordinated_vulnerability_disclosure`, `document_and_change_control`, `incident_management`, `information_security_management` …. +- **Genuin neu (Automotive-spezifisch): 14** — z. B. `approve_production_parts_ppap`, `assess_software_process_capability`, `cybersecurity_management_system`, `document_update_campaigns`, `engineer_requirements_process` …. +- **Lesart:** Spürbare Wiederverwendung (48%) — der Kern beginnt sich zu bilden; der automotive-spezifische Rest (CSMS/SUMS/ASPICE/Funktionssicherheit) ist erwartbar neu. Kein Architekturbruch, sondern ein Hinweis auf den Reifegrad der Capability-Zerlegung. + +## 4. Business Leverage — Märkte + Regulierung, nicht nur „Gesetze" +- `technical_vulnerability_management` erfüllt gleichzeitig **2 Regelwerke** (CRA, UNECE_R155_CSMS) **und** öffnet den **OEM-Markt** (OEM_X_Spec). +- Managementsatz: **„Diese eine Capability erfüllt 2 regulatorische Anforderungen UND ist Eintrittskarte zum OEM-Geschäft"** — überzeugender als „erfüllt 2 Gesetze". (Regulatory Leverage zählt Regelwerke; **Business Leverage** zählt Regelwerke + erschlossene Märkte/Kunden.) + +## Befund + +> **Eine Capability aus bis zu 4 Quellen über 3 Quelltypen — das Modell blieb stabil (0 neue Runtime-Klassen, 0 neue Pipeline; reine Daten).** Convergence ist messbar geworden, und 48% der Automotive-Anforderungen bilden auf BESTEHENDE MCAPs ab — die Registry beginnt zu konvergieren. Nächster Schritt ist bewusst KEINE neue Domäne, sondern **innehalten und die Registry analysieren**: welche MCAPs tauchen domänenübergreifend (CRA·MaschinenVO·OEM·TISAX·ASPICE·Umwelt) am häufigsten auf? Diese hochkonvergenten Capabilities sind der dauerhaft wertvollste Plattformkern. + diff --git a/backend-compliance/reference_scenarios/automotive_convergence_stress_test.py b/backend-compliance/reference_scenarios/automotive_convergence_stress_test.py new file mode 100644 index 00000000..7a1adb4d --- /dev/null +++ b/backend-compliance/reference_scenarios/automotive_convergence_stress_test.py @@ -0,0 +1,127 @@ +# ruff: noqa +# mypy: ignore-errors +"""Automotive convergence stress test — does the SAME capability survive MANY sources? (Phase Ω, test #2) + +Environmental proved domain-agnosticism. This tests a different property: can ONE capability be fed by +many overlapping Requirement Sources at once without the model becoming unstable? Realistic setup — a +supplier with ISO 9001 + IATF 16949 + TISAX + ASPICE + CSMS + SUMS developing an ECU for OEM X. Seven +sources (CRA, UNECE R155/CSMS, R156/SUMS, IATF, TISAX, ASPICE, OEM X) demand heavily OVERLAPPING caps. + +Three new measurements the user asked for: + - Capability Convergence : per capability, how many sources / how many distinct source TYPES feed it. + - Existing-vs-New : how many required caps reuse the EXISTING registry vs are genuinely new + (a registry-convergence signal — high reuse => the core is forming). + - Business Leverage : the most convergent cap satisfies N regulations AND unlocks a market — more + convincing to a GF than "satisfies five laws". + +Data only (a YAML + injected Required caps), zero runtime code. Synthetic, no real names. No deploy. +Run: cd backend-compliance && PYTHONPATH=. python3 reference_scenarios/automotive_convergence_stress_test.py +""" +from __future__ import annotations + +import os +import yaml + +from compliance.company import ( + CompanyContext, Certification, CapabilityMappingEntry, build_company_profile, +) +from compliance.reasoning.enums import Confidence +from compliance.transition_reasoning import ( + TransitionContext, TransitionGoal, TargetRequirement, assess_transition, CoverageStatus, +) + +OUT = [] + + +def w(s=""): + OUT.append(s) + + +_HERE = os.path.dirname(__file__) +A = yaml.safe_load(open(os.path.join(_HERE, "..", "knowledge", "domains", "automotive", "source_capabilities.yaml"), encoding="utf-8")) +SOURCES = A["sources"] + +# ── known capability universe = caps already modelled in the cyber + environmental patterns ── +_K = os.path.join(_HERE, "..", "knowledge", "transition_patterns") +known = set() +for f in os.listdir(_K): + if f.endswith(".yaml"): + p = yaml.safe_load(open(os.path.join(_K, f), encoding="utf-8")) + known |= {a["capability"] for a in p.get("likely_covered", [])} + known |= {d["capability"] for d in p.get("delta_requirements", [])} + +# ── one multi-certified company profile (many overlapping sources) ────────────────────────── +prof_caps = A["company_profile_capabilities"] +cmap = {k: CapabilityMappingEntry(capability_ids=v, confidence=Confidence.MEDIUM) for k, v in prof_caps.items()} +profile = build_company_profile( + CompanyContext(company_id="auto", certifications=[Certification(certification_id=k) for k in prof_caps]), cmap) + +# ── required = union of all source caps for "ECU for OEM X"; delta via the SAME engine ─────── +required = sorted({c for s in SOURCES for c in s["requires"]}) +assess = assess_transition(TransitionContext(company_id="auto", target=TransitionGoal(target_id="ECU_for_OEM_X")), + [TargetRequirement(capability_id=c) for c in required], profile) +delta = sorted({c.capability_id for c in assess.coverage if c.status == CoverageStatus.MISSING}) + +# ── Capability Convergence: per cap -> sources + distinct source types ─────────────────────── +conv = {} +for cap in required: + srcs = [s for s in SOURCES if cap in s["requires"]] + conv[cap] = (sorted(s["id"] for s in srcs), sorted({s["type"] for s in srcs})) +ranked = sorted(required, key=lambda c: (-len(conv[c][0]), -len(conv[c][1]), c)) + +# ── Existing-vs-New (registry convergence signal) ─────────────────────────────────────────── +reuse = sorted(c for c in required if c in known) +fresh = sorted(c for c in required if c not in known) +reuse_pct = round(100 * len(reuse) / len(required)) if required else 0 + +w("# Automotive Convergence Stress Test — überlebt EINE Capability viele Quellen? (Phase Ω #2)") +w("") +w('_Nicht noch eine Domäne, sondern eine andere Eigenschaft: bleibt das Modell stabil, wenn dieselbe Capability gleichzeitig aus vielen überlappenden Quellen gespeist wird? Realistisch: ein Zulieferer (ISO 9001 + IATF 16949 + TISAX + ASPICE + CSMS + SUMS) entwickelt ein Steuergerät für OEM X. Sieben Quellen, bewusste Überlappung. Data-only, keine echten Namen._') +w("") +w("## 1. Sieben überlappende Quellen, eine Engine") +w("- Quellen: %s." % ", ".join("`%s`(%s)" % (s["id"], s["type"]) for s in SOURCES)) +w('- **%d distinct geforderte Capabilities** für „Steuergerät → OEM X" — über dieselbe `assess_transition`-Engine, 0 neue Runtime-Klassen.' % len(required)) +w("- Delta (fehlt dem Profil): **%d** Capabilities." % len(delta)) +w("") + +# ── 2. Capability Convergence ────────────────────────────────────────────── +w("## 2. Capability Convergence — dieselbe Capability aus vielen Quellen") +w("| Capability | Sources | Distinct Source Types | aus |") +w("|---|---:|---:|---|") +for cap in ranked[:8]: + srcs, types = conv[cap] + w("| `%s` | %d | %d | %s |" % (cap, len(srcs), len(types), ", ".join(srcs))) +w("") +top = ranked[0] +w('→ **`%s`** ist am konvergentesten: **%d Quellen** über **%d Quelltypen** — eine Maßnahme, viele Nachweiswelten. Genau hier entsteht der wirtschaftliche Nutzen (nicht „300 Gesetze", sondern „eine Capability ersetzt fünf Nachweiswelten").' % (top, len(conv[top][0]), len(conv[top][1]))) +w("") + +# ── 3. Existing-vs-New (registry convergence) ────────────────────────────── +w("## 3. Existing-vs-New — beginnt die Registry zu konvergieren?") +w("- **Bereits vorhandene MCAPs (Reuse aus Cyber/Umwelt): %d/%d = %d%%** — z. B. %s." % ( + len(reuse), len(required), reuse_pct, ", ".join("`%s`" % c for c in reuse[:5]) + " …")) +w("- **Genuin neu (Automotive-spezifisch): %d** — z. B. %s." % ( + len(fresh), ", ".join("`%s`" % c for c in fresh[:5]) + " …")) +w("- **Lesart:** %s" % ( + "Hohe Wiederverwendung → die Registry konvergiert, ein Kern bildet sich." if reuse_pct >= 60 else + "Spürbare Wiederverwendung (%d%%) — der Kern beginnt sich zu bilden; der automotive-spezifische Rest (CSMS/SUMS/ASPICE/Funktionssicherheit) ist erwartbar neu. Kein Architekturbruch, sondern ein Hinweis auf den Reifegrad der Capability-Zerlegung." % reuse_pct)) +w("") + +# ── 4. Business Leverage ─────────────────────────────────────────────────── +top_srcs, top_types = conv[top] +regs = [s for s in top_srcs if next(x["type"] for x in SOURCES if x["id"] == s) == "regulation"] +contracts = [s for s in top_srcs if next(x["type"] for x in SOURCES if x["id"] == s) == "contract"] +w('## 4. Business Leverage — Märkte + Regulierung, nicht nur „Gesetze"') +w("- `%s` erfüllt gleichzeitig **%d Regelwerke** (%s) **und** öffnet den **OEM-Markt** (%s)." % ( + top, len(regs), ", ".join(regs), ", ".join(contracts) or "—")) +w('- Managementsatz: **„Diese eine Capability erfüllt %d regulatorische Anforderungen UND ist Eintrittskarte zum OEM-Geschäft"** — überzeugender als „erfüllt %d Gesetze". (Regulatory Leverage zählt Regelwerke; **Business Leverage** zählt Regelwerke + erschlossene Märkte/Kunden.)' % (len(regs), len(regs))) +w("") + +# ── Befund ───────────────────────────────────────────────────────────────── +w("## Befund") +w("") +w('> **Eine Capability aus bis zu %d Quellen über %d Quelltypen — das Modell blieb stabil (0 neue Runtime-Klassen, 0 neue Pipeline; reine Daten).** Convergence ist messbar geworden, und %d%% der Automotive-Anforderungen bilden auf BESTEHENDE MCAPs ab — die Registry beginnt zu konvergieren. Nächster Schritt ist bewusst KEINE neue Domäne, sondern **innehalten und die Registry analysieren**: welche MCAPs tauchen domänenübergreifend (CRA·MaschinenVO·OEM·TISAX·ASPICE·Umwelt) am häufigsten auf? Diese hochkonvergenten Capabilities sind der dauerhaft wertvollste Plattformkern.' % ( + len(conv[top][0]), len(conv[top][1]), reuse_pct)) +w("") + +print("\n".join(OUT)) diff --git a/backend-compliance/reference_scenarios/capability_convergence_explanation.md b/backend-compliance/reference_scenarios/capability_convergence_explanation.md new file mode 100644 index 00000000..6d17bd94 --- /dev/null +++ b/backend-compliance/reference_scenarios/capability_convergence_explanation.md @@ -0,0 +1,49 @@ +# Capability Convergence Explanation — WARUM konvergieren diese Capabilities? + +_Nicht „welche MCAPs?", sondern „warum verlangen völlig verschiedene Welten immer wieder DIESELBEN?". Drei abgeleitete Sichten über vorhandene Daten (kein ML, keine neue Architektur). Der eigentliche Burggraben ist nicht „MCAP-X existiert", sondern „warum MCAP-X existieren MUSS"._ + +## 1. Warum konvergieren sie? — Domänen-Matrix + Grund +| Capability | Industrial | Automotive | Medical | Environmental | Grund (Familie) | +|---|---|---|---|---||---| +| `access_control_and_authentication` | ✓ | ✓ | ✓ | – | Wer/was darf — Identität und Zugriff. | +| `sbom_creation` | ✓ | ✓ | ✓ | – | Lieferkette/Stoffstrom — was steckt im Produkt? (SBOM = Software, Stoffliste = Material). | +| `secure_signed_update_distribution` | ✓ | ✓ | ✓ | – | Softwareprodukt — jedes vernetzte Produkt muss sicher aktualisierbar sein. | +| `technical_vulnerability_management` | ✓ | ✓ | ✓ | – | Produktbetrieb — Schwächen über die Lebensdauer behandeln. | +| `asset_and_configuration_management` | ✓ | ✓ | – | – | Konfiguration und Asset-Kontrolle. | +| `coordinated_vulnerability_disclosure` | ✓ | ✓ | – | – | Produktbetrieb — Schwächen über die Lebensdauer behandeln. | +| `cryptography` | ✓ | ✓ | – | – | — | +| `document_and_change_control` | ✓ | ✓ | – | – | Nachweisführung — Konformität dokumentieren. | +| `incident_management` | ✓ | ✓ | – | – | Vorfälle erkennen, behandeln und melden. | +| `product_cyber_risk_assessment` | ✓ | ✓ | – | – | Universeller Prozess — jede Regulierung verlangt eine Risikobeurteilung. | + +→ Das ist keine Statistik mehr, sondern **Erkenntnis**: dieselbe Fähigkeit kehrt wieder, weil ein universelles Prinzip dahinter steht (Softwareprodukt · Lieferkette · Produktbetrieb · universeller Prozess). + +## 2. Capability Families — 75 MCAPs reduzieren sich auf 15 Familien +| Familie | Art | MCAPs | Domänen | Warum universell / spezifisch | +|---|---|---:|---:|---| +| **Risk** | core | 6 | 3 | Universeller Prozess — jede Regulierung verlangt eine Risikobeurteilung. | +| **Update** | core | 4 | 3 | Softwareprodukt — jedes vernetzte Produkt muss sicher aktualisierbar sein. | +| **Vulnerability** | core | 3 | 3 | Produktbetrieb — Schwächen über die Lebensdauer behandeln. | +| **Identity & Access** | core | 3 | 3 | Wer/was darf — Identität und Zugriff. | +| **Inventory & Composition** | core | 6 | 4 | Lieferkette/Stoffstrom — was steckt im Produkt? (SBOM = Software, Stoffliste = Material). | +| **Supplier** | core | 3 | 3 | Lieferantensteuerung — Verantwortung über die Kette. | +| **Incident & Reporting** | core | 2 | 2 | Vorfälle erkennen, behandeln und melden. | +| **Monitoring & Audit** | core | 3 | 3 | Beobachtung — Betrieb und Umfeld überwachen. | +| **Lifecycle & Development** | core | 3 | 3 | Produkt-/Software-Lebenszyklus. | +| **Documentation & Evidence** | core | 6 | 4 | Nachweisführung — Konformität dokumentieren. | +| **Configuration & Asset** | core | 2 | 2 | Konfiguration und Asset-Kontrolle. | +| **Environmental/Material** | domain | 9 | 1 | Umwelt-/Stoff-spezifisch. | +| **Medical** | domain | 7 | 3 | Medizin-/Patientensicherheit-spezifisch. | +| **Automotive** | domain | 4 | 1 | Automotive-/Funktionssicherheit-spezifisch. | +| **Process & QMS** | domain | 4 | 3 | QM-/Prozessdisziplin. | +| _(nicht zugeordnet)_ | — | 10 | — | Review: Familie fehlt oder Name untypisch | + +→ Die Familien **erklären** die Konvergenz: unterschiedliche Regelwerke brauchen dieselben MCAPs, weil sie dieselbe FAMILIE adressieren. Vermutete Langzeit-Reduktion: ~15 Familien statt 75 Einzel-MCAPs. + +## 3. Core vs Domain — eine BERECHNETE Eigenschaft (keine neue Klasse, keine Architektur) +- **Core (6):** kehren über ≥2 unabhängige Domänen UND ≥2 Quelltypen wieder — z. B. `access_control_and_authentication`, `incident_management`, `secure_development_lifecycle`, `secure_signed_update_distribution`, `supplier_security`, `technical_vulnerability_management` …. +- **Domain (61):** überwiegend EINE Fachdomäne — z. B. `account_energy_consumption`, `analyze_water_discharge`, `approve_production_parts_ppap`, `assess_software_process_capability`, `assign_unique_device_identification`, `ce_conformity_assessment_and_technical_documentation` …. +- **Bridging (8):** dazwischen (mehrere Domänen, aber 1 Quelltyp). + +> **Befund:** Medical machte es offensichtlich — die neuen medizinischen Capabilities sind fast alle **Domain**, während Update/SBOM/Access/Logging **Core** sind. Die Zweiteilung ist eine **abgeleitete Eigenschaft** aus (Domänen × Quelltypen), kein neues Modell. Der eigentliche Burggraben ist die Erklärung, WARUM die Core-Familien existieren: sie sind die wenigen universellen Prinzipien (Risiko · Update · Identität · Inventar · Nachweis · Lieferant · Lebenszyklus), auf die sehr unterschiedliche Anforderungswelten immer wieder zurückfallen. Reine Aggregation, 0 Runtime, 0 neue Architektur. + diff --git a/backend-compliance/reference_scenarios/capability_convergence_explanation.py b/backend-compliance/reference_scenarios/capability_convergence_explanation.py new file mode 100644 index 00000000..0fba6151 --- /dev/null +++ b/backend-compliance/reference_scenarios/capability_convergence_explanation.py @@ -0,0 +1,128 @@ +# ruff: noqa +# mypy: ignore-errors +"""Capability Convergence Explanation — WHY do these capabilities converge? (Phase Ω, understand the core) + +Medical proved the registry converges. The mature next step is NOT the next domain but understanding WHY: +not "which MCAPs?" but "why do different worlds keep needing the SAME ones?". Three derived views over +the existing data (no new runtime, no new architecture): + 1. Why converge? — a domain matrix per core MCAP + a curated REASON (the moat: why it must exist) + 2. Capability Families — the ~60 MCAPs reduce to a small set of families, each with a reason + 3. Core vs Domain — a COMPUTED property (not stored): Core recurs across independent domains+types; + Domain stays within one. The split that Medical made obvious. + +Non-runtime -> no deploy. +Run: cd backend-compliance && PYTHONPATH=. python3 reference_scenarios/capability_convergence_explanation.py +""" +from __future__ import annotations + +import os +import yaml + +OUT = [] + + +def w(s=""): + OUT.append(s) + + +_HERE = os.path.dirname(__file__) +_TP = os.path.join(_HERE, "..", "knowledge", "transition_patterns") +FAM = yaml.safe_load(open(os.path.join(_HERE, "..", "knowledge", "capability_families", "families.yaml"), encoding="utf-8"))["families"] + +PATTERN_META = { + "transition_pattern_iso27001_to_cra_maschinenvo_v1.yaml": ("industrial_automation", "regulation", ["CRA", "MaschinenVO"]), + "transition_pattern_iso27001_to_cra_v1.yaml": ("industrial_automation", "regulation", ["CRA"]), + "transition_pattern_iso9001_to_cra_v1.yaml": ("industrial_automation", "regulation", ["CRA"]), + "transition_pattern_isms_to_tisax_v1.yaml": ("automotive", "certification", ["TISAX"]), + "transition_pattern_iso14001_to_environmental_v1.yaml": ("environmental", "regulation", ["ENV"]), + "transition_pattern_iso13485_to_medical_v1.yaml": ("medical", "regulation", ["MDR"]), +} + +idx = {} + + +def _e(c): + return idx.setdefault(c, {"domains": set(), "types": set(), "sources": set()}) + + +for fname, (domain, ttype, default_sources) in PATTERN_META.items(): + p = yaml.safe_load(open(os.path.join(_TP, fname), encoding="utf-8")) + cert = (p.get("transition_goal", {}).get("from", {}) or {}).get("standard", fname) + for a in p.get("likely_covered", []): + e = _e(a["capability"]); e["domains"].add(domain); e["types"].add("certification"); e["sources"].add(cert) + for d in p.get("delta_requirements", []): + e = _e(d["capability"]); e["domains"].add(domain); e["types"].add(ttype); e["sources"] |= set(d.get("covers_targets") or default_sources) + +A = yaml.safe_load(open(os.path.join(_HERE, "..", "knowledge", "domains", "automotive", "source_capabilities.yaml"), encoding="utf-8")) +for s in A["sources"]: + for cap in s["requires"]: + e = _e(cap); e["domains"].add("automotive"); e["types"].add(s["type"]); e["sources"].add(s["id"]) + + +def family_of(cap): + toks = set(cap.split("_")) + for f in FAM: # first family (core first) sharing a token wins + if toks & set(f["tokens"]): + return f + return None + + +DOMAINS = ["industrial_automation", "automotive", "medical", "environmental"] +DLAB = {"industrial_automation": "Industrial", "automotive": "Automotive", "medical": "Medical", "environmental": "Environmental"} + +w("# Capability Convergence Explanation — WARUM konvergieren diese Capabilities?") +w("") +w('_Nicht „welche MCAPs?", sondern „warum verlangen völlig verschiedene Welten immer wieder DIESELBEN?". Drei abgeleitete Sichten über vorhandene Daten (kein ML, keine neue Architektur). Der eigentliche Burggraben ist nicht „MCAP-X existiert", sondern „warum MCAP-X existieren MUSS"._') +w("") + +# ── 1. Why converge? ────────────────────────────────────────────────────── +cross = sorted((c for c, e in idx.items() if len(e["domains"]) >= 2), key=lambda c: (-len(idx[c]["domains"]), c)) +w("## 1. Warum konvergieren sie? — Domänen-Matrix + Grund") +w("| Capability | %s | Grund (Familie) |" % " | ".join(DLAB[d] for d in DOMAINS)) +w("|---|%s|---|" % ("---|" * len(DOMAINS))) +for c in cross[:10]: + cells = ["✓" if d in idx[c]["domains"] else "–" for d in DOMAINS] + f = family_of(c) + w("| `%s` | %s | %s |" % (c, " | ".join(cells), (f["reason"] if f else "—"))) +w("") +w("→ Das ist keine Statistik mehr, sondern **Erkenntnis**: dieselbe Fähigkeit kehrt wieder, weil ein universelles Prinzip dahinter steht (Softwareprodukt · Lieferkette · Produktbetrieb · universeller Prozess).") +w("") + +# ── 2. Capability Families ───────────────────────────────────────────────── +fam_members = {f["id"]: [] for f in FAM} +unassigned = [] +for c in idx: + f = family_of(c) + (fam_members[f["id"]] if f else unassigned).append(c) +w("## 2. Capability Families — %d MCAPs reduzieren sich auf %d Familien" % (len(idx), len([f for f in FAM if fam_members[f['id']]]))) +w("| Familie | Art | MCAPs | Domänen | Warum universell / spezifisch |") +w("|---|---|---:|---:|---|") +for f in FAM: + ms = fam_members[f["id"]] + if not ms: + continue + doms = set() + for c in ms: + doms |= idx[c]["domains"] + w("| **%s** | %s | %d | %d | %s |" % (f["label"], f["kind"], len(ms), len(doms), f["reason"])) +if unassigned: + w("| _(nicht zugeordnet)_ | — | %d | — | Review: Familie fehlt oder Name untypisch |" % len(unassigned)) +w("") +w("→ Die Familien **erklären** die Konvergenz: unterschiedliche Regelwerke brauchen dieselben MCAPs, weil sie dieselbe FAMILIE adressieren. Vermutete Langzeit-Reduktion: ~%d Familien statt %d Einzel-MCAPs." % (len([f for f in FAM if fam_members[f['id']]]), len(idx))) +w("") + +# ── 3. Core vs Domain (COMPUTED, not stored) ────────────────────────────── +core = sorted(c for c, e in idx.items() if len(e["domains"]) >= 2 and len(e["types"]) >= 2) +domain_caps = sorted(c for c, e in idx.items() if len(e["domains"]) == 1) +bridging = [c for c in idx if c not in core and c not in domain_caps] +w("## 3. Core vs Domain — eine BERECHNETE Eigenschaft (keine neue Klasse, keine Architektur)") +w("- **Core (%d):** kehren über ≥2 unabhängige Domänen UND ≥2 Quelltypen wieder — z. B. %s." % ( + len(core), ", ".join("`%s`" % c for c in core[:6]) + " …")) +w("- **Domain (%d):** überwiegend EINE Fachdomäne — z. B. %s." % ( + len(domain_caps), ", ".join("`%s`" % c for c in domain_caps[:6]) + " …")) +w("- **Bridging (%d):** dazwischen (mehrere Domänen, aber 1 Quelltyp)." % len(bridging)) +w("") +w('> **Befund:** Medical machte es offensichtlich — die neuen medizinischen Capabilities sind fast alle **Domain**, während Update/SBOM/Access/Logging **Core** sind. Die Zweiteilung ist eine **abgeleitete Eigenschaft** aus (Domänen × Quelltypen), kein neues Modell. Der eigentliche Burggraben ist die Erklärung, WARUM die Core-Familien existieren: sie sind die wenigen universellen Prinzipien (Risiko · Update · Identität · Inventar · Nachweis · Lieferant · Lebenszyklus), auf die sehr unterschiedliche Anforderungswelten immer wieder zurückfallen. Reine Aggregation, 0 Runtime, 0 neue Architektur.') +w("") + +print("\n".join(OUT)) diff --git a/backend-compliance/reference_scenarios/customer_mission_1.md b/backend-compliance/reference_scenarios/customer_mission_1.md new file mode 100644 index 00000000..99c1bbd8 --- /dev/null +++ b/backend-compliance/reference_scenarios/customer_mission_1.md @@ -0,0 +1,62 @@ +# Customer Mission #1 — Maschinenbauer: „Was muss ich in den nächsten 6 Monaten tun?" + +_KEINE Demo, KEIN Reference Scenario — eine vollständige Simulation eines Beratungsprojekts mit den ECHTEN Engines. Gemessen wird, wie oft der Berater „springen" muss (Sonderlogik statt sauberem Engine-Fluss). Synthetischer Kunde, keine echten Namen._ + +## Der Kunde (synthetisch) +> ISO 9001 · ISMS (ISO 27001) · CE-Prozess · SPS · Fernwartung · Cloud · 80 Entwickler · Export EU +> **Eine Frage:** „Was muss ich in den nächsten sechs Monaten tun?" + +## 1. Scope — was gilt? _(Regulatory Map)_ +- **Gilt:** CRA, MaschinenVO, EMV +- **Unsicher (Rückfrage):** RED, DataAct, NIS2 +- **Overlaps:** VULNERABILITY_HANDLING, SECURITY_UPDATES + +## 2. Journey — welche Übergänge? _(aus Zertifikaten + Zielen)_ +- Hat **ISO 27001 + ISO 9001**, Produkt = vernetzte Maschine → Ziel **CRA + MaschinenVO**. +- Gewählte Journey: **ISO 27001 → CRA + MaschinenVO** (Convergence-Pattern) + QM-Seite ISO 9001 → MaschinenVO. +- ⚠️ Die Übergänge stehen als DATEN in `knowledge/programs/transitions.yaml`, aber **keine Engine wählt sie aus Zertifikaten+Zielen** — hier manuell selektiert. + +## 3. Capability Delta — was fehlt? _(Company 2A + RS-005)_ +> 17 zu klären, 0 bereits abgedeckt, 5 vermutlich vorhanden, 12 fehlt, 0 n/a, 0 nicht im Korpus. +- Vermutlich vorhanden (aus ISMS, Welt 1): incident_management, technical_vulnerability_management, access_control_and_authentication, secure_development_lifecycle … +- Fehlt (Delta): 12 Capabilities, z. B. ce_conformity_assessment_and_technical_documentation, coordinated_vulnerability_disclosure, exploited_vuln_and_incident_reporting, machine_safety_risk_assessment … + +## 4. Roadmap — was zuerst? _(Optimization, größter Hebel)_ +> 16 identifizierte Anforderungen aus 2 Regelwerken -> 12 Massnahmen (Ø Hebel 1.3). +- **Top-Maßnahmen:** `ce_conformity_assessment_and_technical_documentation`(2), `product_cyber_risk_assessment`(2), `protection_against_corruption_of_safety_functions`(2), `secure_signed_update_distribution`(2), `coordinated_vulnerability_disclosure`(1) + +## 5. Playbooks — wie umsetzen? _(Berater-Renderer)_ +- 7 von 12 Maßnahmen haben ein Playbook; 5 brauchen Inhalt (Maschinensicherheits-Playbooks @IACE delegiert). + +## 6. Nachweise — was belegen? _(expected_evidence)_ +- Geforderte Nachweise (Auszug): advisory_process, config_export, cvd_policy, declaration_of_conformity, machine_risk_assessment, operating_instructions … + +## 7. Verification — kann ich es BEWEISEN? +- ⚠️ **Nicht gebaut** — der Verification-Layer (Evidence × Reality → bewiesen) ist Vision V2 (geparkt, Task #45). + +## 8. Completeness — wie sicher/vollständig? _(auditierbar)_ +> Identifiziert 6 · bewertet 2 · offen 4 · Unsicherheiten 1 · Begründung ja +- Offen/begründet: `DataAct`(query_required), `EMV`(future_corpus), `NIS2`(future_corpus), `RED`(future_corpus) + +## Die 6-Monats-Antwort (Beratungsnarrativ) + +> „Sie sind als Maschinenbauer von **CRA + MaschinenVO** (und EMV) betroffen; RED/Data Act/NIS2 sind erst nach **einer Rückfrage** (`generates_usage_data`) zu klären. Ihr ISMS deckt die Informationssicherheits-Seite *wahrscheinlich* ab (zu bestätigen). Offen sind **12 Maßnahmen**. **Wenn Sie in den nächsten 6 Monaten die Top-5 nach regulatorischem Hebel umsetzen, schließen Sie 9 von 16 identifizierten Anforderungen (56%)** — beginnend mit den Maßnahmen, die CRA UND MaschinenVO gleichzeitig erfüllen. Für jede gibt es ein Umsetzungs-Playbook und die geforderten Nachweise; was wir noch NICHT bewerten konnten (EMV/RED/NIS2), weisen wir transparent aus." + +## Flow-Continuity-Audit — der eigentliche Test + +| Übergang | Status | Befund | +|---|---|---| +| Onboarding → Scope | ✅ sauber | Regulatory Map leitet aus dem Produktprofil CRA/MaschinenVO/EMV ab; RED/DataAct/NIS2 unsicher. | +| Scope → Journey | ⚠️ SPRUNG | Kein Selektor-Engine `certs × applicable-targets → journeys` — die Journey-Wahl ist Glue (Daten existieren in transitions.yaml). | +| Journey → Capability Delta | ✅ sauber | assess_transition(Company-Profil, Required) → Coverage + Delta; sauberer Engine-Handoff. | +| Zertifikate → Capabilities (Dependency) | 🔌 Dependency | cert→capability-Map ist Execution-owned + injiziert (hier gemockt) — bewusste Ownership-Grenze, kein Architektur-Bruch. | +| Capability Delta → Roadmap | ✅ sauber | roadmap_from_delta(assessment, covers_targets) → Maßnahmen nach Hebel; sauber. | +| Roadmap → Playbook | ✅ sauber | playbooks_for_plan(plan, knowledge) → Reise je Maßnahme; fehlender Inhalt = ehrliche `missing`-Stubs. | +| Playbook → Evidence | ✅ sauber | expected_evidence trägt aus Pattern/Playbook durch — Datenfeld, kein Bruch. | +| Evidence → Verification | ⚠️ SPRUNG | Verification-Layer fehlt (bewusst geparkt, Vision V2 / Requirements Verification Platform). | +| Completeness (Dependency) | 🔌 Dependency | corpus_status (welche Regelwerke validiert) wird kuratiert/injiziert, nicht aus dem Korpus abgeleitet. | + +**5 sauber · 2 Sprünge · 2 bewusste Dependencies.** + +**Befund:** Die Plattform trägt den **gesamten Beratungsfluss** end-to-end — von der Kundenfrage bis zur priorisierten 6-Monats-Maßnahmenliste mit Playbooks, Nachweisen und ehrlicher Vollständigkeit. **Genau ZWEI echte Sprünge:** (1) **Scope → Journey** — es fehlt ein Selektor-Engine `Zertifikate × Ziele → Journeys` (die Daten existieren, nur die Auswahl ist Glue); (2) **Evidence → Verification** — bewusst geparkter Layer (Vision V2). Die zwei Dependencies (cert→capability-Map @Execution, corpus_status-Kuratierung) sind gewollte Ownership-Grenzen, keine Architektur-Brüche. → **Wenn der Scope→Journey-Selektor steht, ist das Fundament im Wesentlichen fertig; ab dann ist die Arbeit Wissen, nicht Architektur.** + diff --git a/backend-compliance/reference_scenarios/customer_mission_2.md b/backend-compliance/reference_scenarios/customer_mission_2.md new file mode 100644 index 00000000..cb46eb22 --- /dev/null +++ b/backend-compliance/reference_scenarios/customer_mission_2.md @@ -0,0 +1,60 @@ +# Customer Mission #2 — „Wir haben SCHON viel. Was fehlt UNS noch für die CRA?" + +_Zweite Mission, bewusst ANDERS als #1: nicht „ein Zertifikat → ein Ziel", sondern ein hoch-zertifiziertes Unternehmen, das mit einem ganzen Profil ankommt. Test der einzigen offenen Naht aus Mission #1 (Scope → Journey). Synthetischer Kunde, keine echten Namen._ + +## Der Kunde (synthetisch, hoch-zertifiziert) +> **ISO 9001** · **ISO 27001** · **ISO 14001** · **TISAX** · **CE-Prozess** · **PSIRT** · vernetzte Maschinen · Export EU +> **Eine Frage:** „Wir sind schon in vielem zertifiziert — was genau fehlt UNS noch, um CRA-konform zu sein?" + +## 0. Company Capability Profile — der eigentliche Startzustand +> Das Unternehmen bringt **kein Zertifikat als Startpunkt**, sondern ein **Profil**. Jedes Zertifikat ist eine *Beobachtung*, die wahrscheinliche Fähigkeiten beisteuert; der Startzustand ist ihre **Aggregation**. + +| Zertifikat (Beobachtung) | steuert Fähigkeiten bei | Vertrauen | +|---|---|---| +| **ISO27001** — Informationssicherheit (ISMS) | `incident_management`, `technical_vulnerability_management`, `access_control_and_authentication`, `secure_development_lifecycle`, `security_logging_and_monitoring` | medium | +| **TISAX** — Automotive-ISMS — verstärkt dieselben Infosec-Fähigkeiten | `incident_management`, `technical_vulnerability_management`, `access_control_and_authentication`, `secure_development_lifecycle`, `security_logging_and_monitoring` | medium | +| **PSIRT** — Product-Security-Incident-Response — deckt ZWEI CRA-Delta-Fähigkeiten | `coordinated_vulnerability_disclosure`, `exploited_vuln_and_incident_reporting` | high | +| **ISO9001** — QM-Dokumentendisziplin → CE-/Technische-Doku-Fähigkeit | `ce_conformity_assessment_and_technical_documentation` | medium | +| **ISO14001** — Umweltmanagement — im Profil, aber für die CRA NICHT relevant | `environmental_management_documentation` | medium | + +→ **Evidence ist zielrelativ:** ISO 14001 liegt im Profil, hilft der CRA aber **nicht**; PSIRT (oft übersehen) deckt **zwei** CRA-kritische Delta-Fähigkeiten. Genau deshalb darf man **nicht ein Zertifikat** zur Journey machen — das ganze Profil zählt. + +## 1. Ziel _(Intent)_ — was wollen Sie erreichen? +- Intent: **„CRA-konform werden"** → Ziel-Profil = **CRA** (die von der CRA geforderten Fähigkeiten). +- Auswahl-Eingabe ist damit **(Company Profile, Ziel)** — **kein** Zertifikat, das auf eine Journey gemappt wird. + +## 2. Capability Delta — Profil → CRA _(das IST die „Journey")_ +> 17 zu klären, 0 bereits abgedeckt, 8 vermutlich vorhanden, 9 fehlt, 0 n/a, 0 nicht im Korpus. +- **Delta dieses Profils:** 9 fehlende Fähigkeiten. +- **Gegenprobe (nur ISO 27001):** 12 fehlende Fähigkeiten. +- **Mehr Evidence → kleineres Delta:** die zusätzliche Zertifizierung schließt **3** Fähigkeiten *vorab*: `ce_conformity_assessment_and_technical_documentation`, `coordinated_vulnerability_disclosure`, `exploited_vuln_and_incident_reporting`. + +→ Es wurde **keine Journey ausgewählt**. Das Delta `(Profil, Ziel)` IST die Journey — berechnet, nicht gewählt. Die Journey ist nur die *Erklärung* dieses Deltas. + +## 3. Roadmap — was zuerst? _(gleicher Hebel-Engine wie Mission #1)_ +> 12 identifizierte Anforderungen aus 2 Regelwerken -> 9 Massnahmen (Ø Hebel 1.3). +- Top-5 nach Hebel schließen **8 von 12** offenen Anforderungen (67%). + +## Selektions-Rationale — die 5 Fragen (pro Mission zu dokumentieren) + +**Welche Journey wurde gewählt?** +**Keine per-Zertifikat-Journey.** Die Journey ist `Company Capability Profile → CRA`. Gewählt wurde nur das **Ziel** (CRA); der Startzustand wurde aus ALLEN Zertifikaten aggregiert. + +**Warum?** +Bei 6 Zertifikaten würde „ISO 27001 → CRA" die Evidence aus PSIRT/ISO 9001 wegwerfen (= 3 Fähigkeiten, die sonst fälschlich als Delta erschienen). Nur das **ganze Profil** ergibt das korrekte Delta. + +**Welche Informationen waren für die Auswahl entscheidend?** +(a) das **Ziel/Intent** (CRA) und (b) die **vollständige Zertifikatsliste** als Profil — **nicht** ein einzelnes Zertifikat. Welche Evidence hilft, ist **zielrelativ** (ISO 14001 irrelevant, PSIRT hoch-relevant). + +**Musste das Journey-Modell erweitert werden?** +**Konzeptionell ja, strukturell nein.** `from → to` bleibt; aber `from` ist ein **Company Capability Profile** (Multi-Cert-Aggregat), kein Zertifikat. Die Engines (`build_company_profile` + `assess_transition`) tun das BEREITS — es war ein Benenn-/Framing-Fehler, kein fehlender Code. + +**Musste ein neuer Selektionsparameter eingeführt werden?** +**Ja — und er VEREINFACHT.** Eingabe ist `(Company Profile, Ziel)`, nicht `(Zertifikat, Ziel)`. Die Zertifikate kollabieren ins Profil → **keine 2^N Cert-Kombinationen**, nur Profil→Ziel. Der Selektor wird damit kleiner, nicht größer. + +## Was Mission #2 an Mission #1 verändert + +- Mission #1 nannte **Scope → Journey** einen Sprung („kein Selektor `certs × targets → journeys`"). Mission #2 zeigt: **diese Naht schrumpft.** Es gibt keinen Journey-Matcher zu bauen — die Journey ist das **berechnete Delta** `(Profil, Ziel)`. Was real fehlt, ist nur **Profil-Intake + Ziel-Wahl**, nicht eine Journey-Auswahl-Engine. +- Bestätigt den Reframe: **Es gibt keine „ISO 27001 → CRA"-Transition — nur „Company Capability Profile → CRA".** Zertifikate sind **Beobachtungen/Evidence**, kein Journey-Startpunkt. +- **Beobachtung für den (noch nicht gebauten) Selektor:** Eingabe = `(Company Profile, Ziel)`. Diversität über weitere Missionen muss zeigen, ob auch **Produktprofil** und **Intent-Klasse** als Parameter nötig werden — erst dann kanonisieren ([[rule-of-three-canonicalization]]). + diff --git a/backend-compliance/reference_scenarios/customer_mission_3.md b/backend-compliance/reference_scenarios/customer_mission_3.md new file mode 100644 index 00000000..f6773f0f --- /dev/null +++ b/backend-compliance/reference_scenarios/customer_mission_3.md @@ -0,0 +1,40 @@ +# Customer Mission #3 — EIN Profil, DREI Zieltypen (der Requirements-Verification-Beweis) + +_Mission #2 bewies: der Start ist ein Company Capability Profile, kein Zertifikat. Mission #3 beweist das Nächste: dieselbe Pipeline ist ZIELTYP-AGNOSTISCH. Ein Gesetz, eine Zertifizierung und ein Vertrag reduzieren sich alle auf geforderte Fähigkeiten. Synthetischer Kunde + synthetische Ausschreibung, keine echten Namen._ + +## Der Kunde (synthetisch) — EIN Profil +> **ISO 9001 · ISO 27001 · ISO 14001 · TISAX · CE-Prozess · PSIRT** · vernetzte Maschinen · Export EU + +## 1. Drei Zieltypen — dieselbe Engine `Profil − Required = Delta` + +| Ziel | Zieltyp | geforderte Fähigkeiten | Delta (fehlt) | +|---|---|---|---| +| **CRA** | Regulation | 17 | **8** | +| **TISAX** | Certification | 13 | **3** | +| **Öffentliche Ausschreibung** | Contract | 11 | **4** | + +→ Drei **völlig unterschiedliche Zielarten** (Gesetz · Zertifizierung · Vertrag) liefen durch **eine** Engine ohne Sonderfall. Der Vertrag (Ausschreibung) ist nur ein weiterer `Required`-Satz — genau das ist die **Requirements Verification Platform** ([[strategy-requirements-intelligence]]): die Anforderungs-QUELLE ist austauschbar, die Pipeline bleibt. + +## 2. Konvergenz über Zieltypen hinweg +- **8 Fähigkeiten erfüllen ≥2 der drei Zielarten gleichzeitig** — eine Maßnahme zahlt auf Gesetz UND Zertifizierung UND/ODER Vertrag ein. +- Beispiele (Fähigkeit → Ziele): `incident_management`→{CRA,TISAX,Öffentliche Ausschreibung}; `technical_vulnerability_management`→{CRA,Öffentliche Ausschreibung}; `access_control_and_authentication`→{CRA,TISAX,Öffentliche Ausschreibung}; `sbom_creation`→{CRA,Öffentliche Ausschreibung} +- Das ist der Hebel *eine Ebene höher* als Mission #1: dort konvergierten **Gesetze** (CRA+MaschinenVO), hier konvergieren **ZielTYPEN**. + +## 3. Evidence-Relevanz(Ziel) — dieselbe Zertifizierung, anderer Wert je Ziel +> Nicht „Evidence vorhanden", sondern **Evidence-Relevanz(Ziel)**: jede Zertifizierung wird **relativ zum Ziel** bewertet. Das erklärt, warum dieselbe Capability in zwei Beratungen unterschiedlich priorisiert wird. + +| Zertifizierung (Evidence) | → CRA | → TISAX | → Ausschreibung | +|---|---|---|---| +| **ISO27001** | hoch (5) | hoch (5) | hoch (4) | +| **TISAX** | mittel (2) | hoch (8) | hoch (5) | +| **PSIRT** | hoch (3) | keine (0) | mittel (1) | +| **ISO9001** | mittel (1) | keine (0) | keine (0) | +| **ISO14001** | keine (0) | keine (0) | keine (0) | +| **CE** | mittel (1) | keine (0) | keine (0) | + +→ **PSIRT** ist gegen die **CRA hoch**, gegen **TISAX keine**, gegen die **Ausschreibung mittel** — dieselbe Evidence, drei verschiedene Werte. **ISO 14001** ist gegen alle drei (Security/Qualität) **keine** — *aber* gegen ein **Umwelt-Ziel** (Batterieverordnung/Umweltauflagen) wäre sie **hoch**. Genau deshalb gilt: **Relevanz ist eine Funktion des Ziels, kein Attribut der Evidence.** + +## Befund + +> **Dieselbe Pipeline trägt Gesetz, Zertifizierung UND Vertrag.** Damit ist bewiesen, dass die Architektur nicht „Compliance" macht, sondern **Anforderungen verifiziert** — die Quelle (Regulation/Certification/Contract) ist nur ein `Required`-Satz, der Rest ist `Profil − Required = Delta`. Zwei durable Folgerungen: (1) **Evidence-Relevanz ist zielrelativ** (gehört künftig als `relevance(evidence, target)` modelliert, nicht als „vorhanden/fehlt"); (2) Konvergenz existiert nicht nur zwischen Gesetzen, sondern zwischen **Zielarten** — der höchste Hebel überhaupt. + diff --git a/backend-compliance/reference_scenarios/customer_mission_4.md b/backend-compliance/reference_scenarios/customer_mission_4.md new file mode 100644 index 00000000..2462df3b --- /dev/null +++ b/backend-compliance/reference_scenarios/customer_mission_4.md @@ -0,0 +1,39 @@ +# Customer Mission #4 — zwei verschiedene Verträge, eine Engine (kein Contract-Spezialfall) + +_Mission #3 zeigte EINEN Vertrag (öffentliche Ausschreibung) durch dieselbe Engine wie Gesetz/Zertifizierung. Ein Beispiel reicht nicht — sonst backen wir Tender-Annahmen in den späteren Selektor. Hier laufen ZWEI bewusst unterschiedliche Vertragsarten gegen dasselbe Unternehmen. Synthetischer Kunde + synthetische Verträge, keine echten Namen._ + +## Der Kunde (synthetisch) — EIN Profil +> **ISO 9001 · ISO 27001 · ISO 14001 · TISAX · CE-Prozess · PSIRT** · vernetzte Maschinen · Export EU + +## 1. Zwei Vertragsarten — dieselbe Engine `Profil − Required = Delta` + +| Vertrag | Art | geforderte Fähigkeiten | Delta (fehlt) | +|---|---|---|---| +| **Öffentliche Ausschreibung** | public tender | 10 | **4** | +| **OEM-Lastenheft** | private OEM spec | 11 | **3** | + +→ Beide Verträge sind nur ein `Required`-Satz; **es gibt keinen Contract-spezifischen Codepfad** — `assess_transition` behandelt sie identisch zu Gesetz und Zertifizierung. + +## 2. Verschiedene Verträge → verschiedene Deltas (Beweis: keine Tender-Speziallogik nötig) +- **Nur Ausschreibung:** `penetration_test_evidence`, `reference_project_evidence`, `sbom_creation`, `security_sla_and_support_commitment` +- **Nur OEM-Lastenheft:** `aspice_process_capability`, `functional_safety_evidence`, `software_update_management_system` +- **Beiden gemeinsam:** — + +→ Die zwei Verträge fordern **strukturell anderes** (Beschaffungsnachweise vs. Automotive-Engineering: CSMS/funktionale Sicherheit/SUMS/ASPICE). Trotzdem **ein** Mechanismus. Genau das brauchten wir vor dem Selektor: zwei diverse Contract-Ziele, kein Sonderpfad. + +## 3. Evidence-Relevanz(Vertrag) +| Zertifizierung (Evidence) | → Ausschreibung | → OEM-Lastenheft | +|---|---|---| +| **ISO27001** | hoch (4) | hoch (4) | +| **TISAX** | hoch (4) | hoch (6) | +| **PSIRT** | mittel (1) | mittel (2) | +| **ISO9001** | keine (0) | keine (0) | +| **ISO14001** | keine (0) | keine (0) | +| **CE** | keine (0) | keine (0) | + +→ **TISAX** zählt gegen den **Automotive-OEM** mehr als gegen die generische Ausschreibung (Prototype Protection, CSMS); **ISO 14001** ist gegen beide **keine**. Bestätigt: **Relevanz ist eine Funktion des Ziels** — auch zwischen zwei Verträgen. + +## Befund + +> **Zwei strukturell verschiedene Verträge, ein Mechanismus, keine Zeile Contract-Spezialcode.** Damit ist „Contract" als Anforderungsquelle abgesichert (≥2 diverse Fälle): der spätere Scope→Journey-Selektor kann **jeden** Vertrag als reinen `Required`-Satz behandeln, ohne Tender-Speziallogik. Nächste sinnvolle Diversität vor dem Selektor: ein Vertrag, der bewusst auf NICHT-Security-Fähigkeiten zielt (z. B. Umwelt-/Materialnachweise), um auch die zielrelative Evidence-Relevanz über Domänen hinweg zu prüfen. + diff --git a/backend-compliance/reference_scenarios/customer_mission_5.md b/backend-compliance/reference_scenarios/customer_mission_5.md new file mode 100644 index 00000000..77d1d26f --- /dev/null +++ b/backend-compliance/reference_scenarios/customer_mission_5.md @@ -0,0 +1,27 @@ +# Customer Mission #5 — ein Nicht-Security-Ziel: kippt die Evidence-Relevanz? + +_Enger Scope: KEIN Umweltrecht, KEINE ISO-14001-Normmodellierung, KEIN neues Modul, KEIN Deploy. Nur die EINE Frage: ist `relevance(evidence, target)` wirklich eine Funktion des Ziels — oder ein Attribut der Evidence? Das Umwelt-/Materialnachweis-Ziel ist ein hand-authored `Required`-Satz (synthetisch), nur um die bestehende Engine auf ein Nicht-Security-Ziel zu richten. Synthetischer Kunde, keine echten Namen._ + +## Der Kunde (synthetisch) — EIN Profil (hat u. a. ISO 14001) +> **ISO 9001 · ISO 27001 · ISO 14001 · TISAX · PSIRT** · vernetzte Maschinen · Export EU + +## 1. Evidence-Relevanz über drei Ziele — zwei Security, eines NICHT +| Zertifizierung (Evidence) | → CRA | → TISAX | → Umwelt-/Material | +|---|---|---|---| +| **ISO27001** | hoch (5) | hoch (3) | keine (0) | +| **TISAX** | niedrig (2) | hoch (6) | keine (0) | +| **PSIRT** | hoch (3) | keine (0) | keine (0) | +| **ISO14001** | keine (0) | keine (0) | hoch (3) | ⟵ kippt +| **ISO9001** | niedrig (1) | keine (0) | keine (0) | + +## 2. Beweis — Relevanz ist eine Funktion des ZIELS (in BEIDE Richtungen) +- **ISO 14001:** gegen CRA/TISAX **keine**, gegen das Umwelt-/Materialziel **hoch (3)**. Dieselbe Zertifizierung — von wertlos zu entscheidend, nur weil das Ziel wechselt. +- **Symmetrisch:** **ISO 27001** (hoch gegen CRA/TISAX) ist gegen das Umwelt-/Materialziel **keine (0)**; **PSIRT** ebenso **keine (0)**. Security-Evidence ist hier wertlos. +- Delta des Umwelt-/Materialziels: **5** fehlende Fähigkeiten (das Profil deckt nur die ISO-14001-nahen ab) — über dieselbe `assess_transition`-Engine, kein Sonderpfad. + +→ **Damit ist `relevance(evidence, target)` zweiseitig bewiesen:** keine Evidence ist „an sich" relevant; Relevanz entsteht erst gegen ein Ziel. Eine Capability/Zertifizierung ohne Ziel hat keinen Relevanzwert. + +## Befund + +> **Ein und dieselbe Evidence kann je Ziel wertlos oder hoch relevant sein** — hier erstmals an einem NICHT-Security-Ziel gezeigt, in beide Richtungen (ISO 14001 kippt von keine→hoch, Security-Certs von hoch→keine). Folgerung für das spätere Modell: Relevanz darf NICHT als Attribut der Evidence gespeichert werden, sondern nur als `relevance(evidence, target)` berechnet (computed-not-stored). **Damit ist die Ziel-Diversität für den späteren Selektor beisammen: Regulation · Certification · Contract/Tender · OEM-Spec · Umwelt-/Material-Ziel — fünf Zielarten durch dieselbe Engine. Erst jetzt wird ein Scope→Journey-Selektor sinnvoll** (er optimiert nicht mehr auf einer einzigen Zielart). + diff --git a/backend-compliance/reference_scenarios/environmental_stress_test.md b/backend-compliance/reference_scenarios/environmental_stress_test.md new file mode 100644 index 00000000..b5669e84 --- /dev/null +++ b/backend-compliance/reference_scenarios/environmental_stress_test.md @@ -0,0 +1,37 @@ +# Environmental Stress Test — funktioniert die Architektur AUSSERHALB von Cyber? (Phase Ω) + +_Erster Nicht-Cyber-Test. Nicht „wir bauen einen Umwelt-Cluster", sondern: trägt RS-005 ein völlig anderes Denkmodell (Stoffe/Emissionen/Wasser/Energie/Kreislauf) UNVERÄNDERT — nur neue DATEN, null Runtime-Code? ISO 14001 als Company Profile (Welt-1), dieselbe Engine wie ISO 27001 → CRA. Synthetisch, keine echten Namen._ + +## 1. ISO 14001 als Company Profile — Management, nicht Evidence +- ISO 14001 liefert **Umwelt-MANAGEMENT-Disziplin** (Welt-1, wahrscheinlich vorhanden): `identify_environmental_aspects`, `operate_environmental_compliance_process`, `conduct_internal_environmental_audits`, `run_continual_environmental_improvement`, `control_environmental_documents`. +- Über **dieselbe** `assess_transition`-Engine wie im Cyber-Fall — **keine Zeile neuer Runtime-Code**, nur ein neues Pattern-YAML. + +## 2. RS-005 stellt dieselbe Frage: welche Umwelt-Capabilities fehlen? +> 16 zu klären, 0 bereits abgedeckt, 5 vermutlich vorhanden, 11 fehlt, 0 n/a, 0 nicht im Korpus. +- **Vermutlich vorhanden (Management):** 5 — `conduct_internal_environmental_audits`, `control_environmental_documents`, `identify_environmental_aspects` … +- **Delta (konkrete Evidence, fehlt): 11 Capabilities** — z. B. `account_energy_consumption`, `analyze_water_discharge`, `declare_material_composition`, `document_waste_streams` … +- Capabilities sind **Verben** (capability-is-a-verb): `manage_chemical_substances`, `measure_air_emissions`, `issue_battery_passport` … + +## 3. Neue Qualitätsfrage — was erzeugt ISO 14001 typischerweise NICHT? _(rejected_assumptions, Welt-1/Welt-2)_ +- ISO 14001 does NOT produce concrete substance lists or REACH registrations. +- ISO 14001 does NOT produce concrete air-emission measurements. +- ISO 14001 does NOT produce battery passports or material declarations. +- ISO 14001 does NOT produce water or wastewater analyses. +- An ISO 14001 certificate does NOT establish RoHS substance-restriction evidence. + +→ Genau wie ISO 9001 → CRA: ein **Managementsystem** gibt die Disziplin, aber **nicht die konkrete substanz-/produktspezifische Evidence**. Die Welt-1/Welt-2-Trennung bleibt erhalten. + +## 4. Journey Matcher bleibt domänen-agnostisch +> 1 Journeys erklaeren das Delta; beste: ISO14001 -> Environmental (100% des Deltas) +| Journey | erklärt das Umwelt-Delta | +|---|---| +| ISO14001 -> Environmental | 100% | +| ISMS -> TISAX | 0% | +| ISO27001 -> CRA + MaschinenVO | 0% | + +→ Die **Cyber-Journeys erklären 0 %** des Umwelt-Deltas — der Matcher rät nicht, er erklärt nur, was das Delta wirklich beschreibt. + +## Befund + +> **Ein völlig anderes Denkmodell (Umwelt) lief durch `Reality → Evidence → Capability → Required → Delta → Journey` ohne eine Zeile neuen Runtime-Code — nur ein neues Pattern-YAML + injizierte Required-Caps.** Das ist ein stärkerer Generalitätsbeweis als zehn weitere Cyber-Regelwerke: die Architektur ist nicht „Compliance/Cyber", sondern ein allgemeines Trägersystem. **16 neue Capability-Typen** (5 Management + 11 konkrete Evidence) — in der Größenordnung der Cyber-Domänen, kein Granularitäts-Frühindikator. Architecture Stability: **0 neue Runtime-Klassen, 0 neue Pipeline.** + diff --git a/backend-compliance/reference_scenarios/environmental_stress_test.py b/backend-compliance/reference_scenarios/environmental_stress_test.py new file mode 100644 index 00000000..fb80d41a --- /dev/null +++ b/backend-compliance/reference_scenarios/environmental_stress_test.py @@ -0,0 +1,107 @@ +# ruff: noqa +# mypy: ignore-errors +"""Environmental stress test — does the architecture work OUTSIDE cyber? (Phase Ω) + +Every prior journey lived in the cyber family (CRA / MaschinenVO / TISAX / ISO 27001 / OEM / Tender — +all infosec, software, product cybersecurity). This is the first NON-cyber stress test: substance flows, +emissions, water, chemicals, energy, circularity. The claim under test is NOT "we built an environmental +cluster" but "RS-005 carries an entirely different mental model UNCHANGED — only new DATA, zero runtime". + +It runs ISO 14001 (an EMS, as a Company Profile, Welt-1) -> an Environmental target through the SAME +engines used for ISO 27001 -> CRA, and asks the SAME question: which environmental capabilities are +still missing? Plus the new quality question (rejected_assumptions): which capabilities does ISO 14001 +typically NOT produce? And it runs the Journey Matcher to confirm it stays domain-agnostic. + +Synthetic, no real names. Non-runtime -> no deploy. +Run: cd backend-compliance && PYTHONPATH=. python3 reference_scenarios/environmental_stress_test.py +""" +from __future__ import annotations + +import os +import yaml + +from compliance.company import ( + CompanyContext, Certification, CapabilityMappingEntry, build_company_profile, +) +from compliance.reasoning.enums import Confidence +from compliance.transition_reasoning import ( + TransitionContext, TransitionGoal, TargetRequirement, assess_transition, CoverageStatus, +) +from compliance.journey_matcher import JourneySignature, match_journeys + +OUT = [] + + +def w(s=""): + OUT.append(s) + + +_K = os.path.join(os.path.dirname(__file__), "..", "knowledge", "transition_patterns") + + +def _load(name): + return yaml.safe_load(open(os.path.join(_K, name), encoding="utf-8")) + + +ENV = _load("transition_pattern_iso14001_to_environmental_v1.yaml") +mgmt = [a["capability"] for a in ENV["likely_covered"]] # what ISO 14001 DOES give (Welt-1) +concrete = [d["capability"] for d in ENV["delta_requirements"]] # what it does NOT give (the delta) + +# ── Company Profile: ISO 14001 -> the management capabilities only (NO concrete evidence) ──── +profile = build_company_profile( + CompanyContext(company_id="env", certifications=[Certification(certification_id="ISO14001")]), + {"ISO14001": CapabilityMappingEntry(capability_ids=mgmt, confidence=Confidence.MEDIUM)}) + +# ── SAME engine as ISO 27001 -> CRA: required = management + concrete; delta = what's missing ─ +reqs = [TargetRequirement(capability_id=c) for c in mgmt + concrete] +assess = assess_transition(TransitionContext(company_id="env", target=TransitionGoal(target_id="Environmental")), reqs, profile) +covered = sorted({c.capability_id for c in assess.coverage if c.status != CoverageStatus.MISSING}) +delta = sorted({c.capability_id for c in assess.coverage if c.status == CoverageStatus.MISSING}) + +# ── Journey Matcher: stays domain-agnostic? cyber journeys must score ~0 on an env delta ───── +def _sig(name, label): + p = _load(name) + return JourneySignature(journey_id=p.get("id", name), label=label, + capability_pattern=[d["capability"] for d in p["delta_requirements"]]) +journeys = [ + _sig("transition_pattern_iso14001_to_environmental_v1.yaml", "ISO14001 -> Environmental"), + _sig("transition_pattern_iso27001_to_cra_maschinenvo_v1.yaml", "ISO27001 -> CRA + MaschinenVO"), + _sig("transition_pattern_isms_to_tisax_v1.yaml", "ISMS -> TISAX"), +] +match = match_journeys(delta, journeys) + +w("# Environmental Stress Test — funktioniert die Architektur AUSSERHALB von Cyber? (Phase Ω)") +w("") +w('_Erster Nicht-Cyber-Test. Nicht „wir bauen einen Umwelt-Cluster", sondern: trägt RS-005 ein völlig anderes Denkmodell (Stoffe/Emissionen/Wasser/Energie/Kreislauf) UNVERÄNDERT — nur neue DATEN, null Runtime-Code? ISO 14001 als Company Profile (Welt-1), dieselbe Engine wie ISO 27001 → CRA. Synthetisch, keine echten Namen._') +w("") +w("## 1. ISO 14001 als Company Profile — Management, nicht Evidence") +w("- ISO 14001 liefert **Umwelt-MANAGEMENT-Disziplin** (Welt-1, wahrscheinlich vorhanden): %s." % ", ".join("`%s`" % c for c in mgmt)) +w("- Über **dieselbe** `assess_transition`-Engine wie im Cyber-Fall — **keine Zeile neuer Runtime-Code**, nur ein neues Pattern-YAML.") +w("") +w("## 2. RS-005 stellt dieselbe Frage: welche Umwelt-Capabilities fehlen?") +w("> %s" % assess.summary.headline) +w("- **Vermutlich vorhanden (Management):** %d — %s" % (len(covered), ", ".join("`%s`" % c for c in covered[:3]) + " …")) +w("- **Delta (konkrete Evidence, fehlt): %d Capabilities** — z. B. %s …" % (len(delta), ", ".join("`%s`" % c for c in delta[:4]))) +w("- Capabilities sind **Verben** (capability-is-a-verb): `manage_chemical_substances`, `measure_air_emissions`, `issue_battery_passport` …") +w("") +w("## 3. Neue Qualitätsfrage — was erzeugt ISO 14001 typischerweise NICHT? _(rejected_assumptions, Welt-1/Welt-2)_") +for r in ENV["rejected_assumptions"]: + w("- %s" % r) +w("") +w("→ Genau wie ISO 9001 → CRA: ein **Managementsystem** gibt die Disziplin, aber **nicht die konkrete substanz-/produktspezifische Evidence**. Die Welt-1/Welt-2-Trennung bleibt erhalten.") +w("") +w("## 4. Journey Matcher bleibt domänen-agnostisch") +w("> %s" % match.headline) +w("| Journey | erklärt das Umwelt-Delta |") +w("|---|---|") +for m in match.matches: + w("| %s | %d%% |" % (m.label, round(m.score * 100))) +w("") +w("→ Die **Cyber-Journeys erklären 0 %** des Umwelt-Deltas — der Matcher rät nicht, er erklärt nur, was das Delta wirklich beschreibt.") +w("") +w("## Befund") +w("") +w('> **Ein völlig anderes Denkmodell (Umwelt) lief durch `Reality → Evidence → Capability → Required → Delta → Journey` ohne eine Zeile neuen Runtime-Code — nur ein neues Pattern-YAML + injizierte Required-Caps.** Das ist ein stärkerer Generalitätsbeweis als zehn weitere Cyber-Regelwerke: die Architektur ist nicht „Compliance/Cyber", sondern ein allgemeines Trägersystem. **%d neue Capability-Typen** (5 Management + %d konkrete Evidence) — in der Größenordnung der Cyber-Domänen, kein Granularitäts-Frühindikator. Architecture Stability: **0 neue Runtime-Klassen, 0 neue Pipeline.**' % (len(mgmt) + len(concrete), len(concrete))) +w("") + +print("\n".join(OUT)) diff --git a/backend-compliance/reference_scenarios/generate.py b/backend-compliance/reference_scenarios/generate.py new file mode 100644 index 00000000..77be2392 --- /dev/null +++ b/backend-compliance/reference_scenarios/generate.py @@ -0,0 +1,493 @@ +# ruff: noqa +# mypy: ignore-errors +"""Reference Scenario Suite v1 — generator (living regression reference). + +Runs the REAL deployed engines for three real customer scenarios and emits a +markdown reference artifact. Per scenario it derives an "Architecture Coverage" +table from the actual run, so when a new domain lands the cells flip automatically +(e.g. Sz2/Environmental UNSUPPORTED -> PASS). This is the objective measure of +"is BreakPilot better than six months ago" — not LOC, not features. + +Run: cd backend-compliance && PYTHONPATH=. python3 reference_scenarios/generate.py +Not product code; not imported by the app. +""" +from __future__ import annotations + +from typing import List, Tuple + +from compliance.profile.canonical import ( + CanonicalProductRegulatoryProfile as P, CanonicalProductType as PT, + EconomicOperatorRole as Role, CanonicalLifecyclePhase as LP, + ProductComponent as Comp, ComponentKind as CK, EnvironmentalImpact as Env, +) +from compliance.product_scope.orchestrator import resolve_product_scope +from compliance.product_scope.schemas import ScopeStatus +from compliance.regulatory_map.renderer import render_regulatory_map +from compliance.interpretation_map.adapter import interpret_in_map +from compliance.rci import create_baseline, assess_change, RegulatoryChange, ChangeType +from compliance.company import ( + CompanyContext, Certification, Declaration, ExistingEvidence, + CapabilityMappingEntry, build_company_profile, +) +from compliance.capability import ( + CapabilityRegistry, CapabilityCandidate, CapabilityRelation, RelationType, + EvidenceKind, mint_capability, evaluate_relation, +) +from compliance.reasoning.enums import Confidence +from compliance.transition_reasoning import ( + TransitionContext, TransitionGoal, TargetType, TargetRequirement, assess_transition, CoverageStatus, + regulatory_convergence, +) +from compliance.optimization import roadmap_from_delta, select_within_budget +from compliance.playbook import playbooks_for_plan +from compliance.knowledge_production import drafts_from_pattern +import os +import yaml + +from _helpers import ( # noqa: E402 (script-dir module; keeps generate.py under the LOC budget) + OUT, ROLLUP, Row, w, coverage_table, reg_map_block, unsupported_block, interp_status, + knowledge_intake_section, completeness_section, domain_programs_section, + transition_coverage_section, +) + +ISO_MAP = {"ISO27001": CapabilityMappingEntry( + capability_ids=["cap_incident_response", "cap_supplier_management", "cap_asset_management"], + confidence=Confidence.MEDIUM)} + + +w("# Reference Scenario Suite v1") +w("") +w("> **Kein Doku-Artefakt — die erste Ground Truth / Living Reference Suite.** Erzeugt aus den " + "REALEN deployten Engines (aktueller deployter main) via `reference_scenarios/generate.py`. Jede " + "`Architecture Coverage`-Zelle ist aus dem echten Lauf ABGELEITET; sobald eine Domaene landet, " + "kippt die Zelle automatisch (z. B. Sz2/Environmental UNSUPPORTED -> PASS). Beantwortet dauerhaft: " + '„Ist BreakPilot besser als vor sechs Monaten?" — anhand echter Kundensituationen, nicht LOC.') +w("") +w("Synthetische `Cert->Capability`-Mappings sind als ILLUSTRATIV markiert; die echte Tabelle gehoert " + "Compliance Execution (siehe Master Capability Registry).") +w("") + +# ── Scenario 1 ──────────────────────────────────────────────────────────── +w("## Szenario 1 — Maschinenbauer mit ISMS + SBOM + Remote Access") +w("") +w('**Frage:** „Was gilt fuer uns, und reicht das?"') +w("") +p1 = P(name="Industrielle Verpackungsmaschine", product_type=PT.MACHINERY, markets=["EU", "DE"], + economic_operator_role=Role.MANUFACTURER, lifecycle_phase=LP.PLACING_ON_MARKET, + is_machine=True, is_component=False, has_software_updates=True, has_embedded_software=True, + has_remote_access=True, connected_to_internet=True, has_security_function=True, + has_sbom=True, has_incident_response=True, technologies=["cloud", "ota_updates"], + existing_certifications=["ISO27001"]) +sc1 = resolve_product_scope(p1) +rmap1 = render_regulatory_map(p1) +w("**Input:** Maschine, vernetzt (Remote/Cloud), Firmware, Rolle Hersteller, Maerkte EU/DE; " + "Company: ISMS (ISO27001) + SBOM + Incident Response.") +w("") +reg_map_block(rmap1) +w("**Input — Company Context** _(ILLUSTRATIVES Mapping: ISO27001 -> incident_response, supplier_management, asset_management)_") +w("") +ctx1 = CompanyContext(company_id="maschinenbau", + certifications=[Certification(certification_id="ISO27001", name="ISO/IEC 27001")], + declarations=[Declaration(capability_id="cap_patch_management")], + evidence=[ExistingEvidence(evidence_id="sbom.json", evidence_type="sbom", proves_capability_id="cap_sbom_management")]) +prof1 = build_company_profile(ctx1, ISO_MAP) +for c in prof1.candidate_capabilities: + w("- candidate: %s — %s (%s)" % (c.capability_id, c.verification_status.value, c.source)) +for c in prof1.confirmed_capabilities: + w("- CONFIRMED: %s — %s (Nachweis: %s)" % (c.capability_id, c.verification_status.value, ", ".join(c.sources))) +w("") +ie1 = interpret_in_map(rmap1, "Wir liefern Sicherheitsupdates fuenf Jahre lang, damit ist der CRA erfuellt.") +w("**Expected Interpretation**") +w("") +w('> Auslegung „5 Jahre Updates -> CRA erfuellt" -> **%s**' % ie1.assessment.value) +w("> " + ie1.explanation) +w("") +base1 = create_baseline(p1, {"sbom_creation": ["sbom"], "provide_security_updates": ["policy"]}, baseline_id="s1") +a1 = assess_change(base1, RegulatoryChange(change_id="cra-2026-amd", affected_regulations=["CRA"], + affected_obligations=["cra_new_disclosure_duty", "sbom_creation", "vuln_handling_process"], + change_type=ChangeType.AMENDMENT)) +w("**Expected RCI** _(CRA-Novelle gegen gespeicherte Baseline)_") +w("") +w("> affects_product = %s — %s" % (a1.affects_product, a1.summary.what_changed)) +for d in a1.deltas: + w("- %s -> **%s** (fehlende Nachweise: %s)" % (d.obligation_id, d.delta_type.value, ", ".join(d.missing_evidence) or "-")) +w("") +unsupported_block(rmap1) +w("**Known Gaps:** Interpretation kennt kein CRA-Muster (RS-001) · MaschinenVO/EMV-Pflichten nicht " + "registry-verlinkt (RS-004) · cap/MCAP/Pflicht-Evidence nicht gejoint = Company-Gap (RS-003).") +w("") +coverage_table([ + ("Company Context", "PASS", "ISO27001 + SBOM + Declaration"), + ("Product Profile", "PASS", "Maschine, vernetzt, Firmware"), + ("Navigator", "PASS", "ready_for_scope"), + ("Scope", "PASS" if sc1.status == ScopeStatus.RESOLVED else "NEEDS_FACTS", "CRA/MaschinenVO/EMV + 3 unsicher"), + ("Regulatory Map", "PASS", "Overlaps + 1-Nachweis-N-Pflichten"), + ("CRA Obligations", "PASS", "12 registry-verlinkt"), + ("MaschinenVO/EMV Obligations", "PARTIAL", "Scope ja, Pflichten nicht verlinkt → RS-004"), + ("Interpretation", interp_status(ie1.assessment.value), "kein CRA-Muster → RS-001"), + ("RCI", "PASS", "1 neu, 2 geaendert"), + ("Company Gap", "TODO", "cap↔MCAP↔Pflicht nicht gejoint → RS-003"), + ("Environmental", "N/A", "keine Umwelt-Trigger"), +]) + +# ── Scenario 2 ──────────────────────────────────────────────────────────── +w("## Szenario 2 — Industriespuelmaschine mit Abwasser/Chemikalien") +w("") +w('**Frage:** „Welche Umweltbereiche sind noch nicht abgedeckt?"') +w("") +p2 = P(name="Industriespuelmaschine", product_type=PT.MACHINERY, markets=["EU", "DE"], + economic_operator_role=Role.MANUFACTURER, lifecycle_phase=LP.PLACING_ON_MARKET, + is_machine=True, is_component=False, has_software_updates=True, has_embedded_software=True, + components=[Comp(name="Dosierpumpe", kind=CK.CHEMICAL_DOSING), Comp(name="Abwasserauslass", kind=CK.WASTEWATER_OUTLET)], + environmental=Env(discharges_to_wastewater=True, uses_cleaning_chemicals=True, consumes_energy_or_water=True)) +sc2 = resolve_product_scope(p2) +rmap2 = render_regulatory_map(p2) +w("**Input:** Maschine mit Chemikalien-Dosierung + Abwasserauslass; Umwelt-Trigger gesetzt.") +w("") +reg_map_block(rmap2) +unsupported_block(rmap2) +ie2 = interpret_in_map(rmap2, "Unsere Abwassereinleitung mit Reinigungschemikalien ist sicher abgedeckt.") +w("**Expected Interpretation** _(Umwelt -> bewusst nicht bewertet)_") +w("") +w("> " + ie2.explanation) +w("") +w("**Known Gaps:** Abwasser/Chemikalien/Energie sind `unsupported_domain` — Environmental Corpus fehlt (RS-002).") +w("") +env_status = "UNSUPPORTED" if rmap2.unsupported_domains else "PASS" +coverage_table([ + ("Product Profile", "PASS", "Maschine + Umwelt-Komponenten"), + ("Scope", "PASS" if sc2.status == ScopeStatus.RESOLVED else "NEEDS_FACTS", ""), + ("Regulatory Map", "PASS", "CRA/MaschinenVO/EMV"), + ("Environmental (Abwasser/Chemikalien/Energie)", env_status, 'ehrlich „noch nicht im Korpus" → RS-002'), + ("Interpretation (Umwelt)", "PARTIAL", "future_corpus_needed statt Scheinsicherheit"), +]) + +# ── Scenario 3 ──────────────────────────────────────────────────────────── +w("## Szenario 3 — ISO27001-zertifiziertes Unternehmen") +w("") +w('**Frage:** „Welche Capabilities sind inferred, declared oder confirmed?"') +w("") +w("_ILLUSTRATIVES Mapping: ISO27001 -> incident_response, supplier_management, asset_management_") +w("") +ctx3 = CompanyContext(company_id="iso-kunde", + certifications=[Certification(certification_id="ISO27001", name="ISO/IEC 27001")], + declarations=[Declaration(capability_id="cap_patch_management", statement="Wir machen Patch Management")], + evidence=[ExistingEvidence(evidence_id="patch-policy.pdf", evidence_type="policy", proves_capability_id="cap_patch_management")]) +prof3 = build_company_profile(ctx3, ISO_MAP) +w("**Expected Company Capability Profile** _(4-Zustands-Trust-Model)_") +w("") +for c in prof3.candidate_capabilities: + w("- %s — **%s** (Quelle: %s)" % (c.capability_id, c.verification_status.value, c.source)) +for c in prof3.confirmed_capabilities: + w("- %s — **%s** (Nachweis: %s)" % (c.capability_id, c.verification_status.value, ", ".join(c.sources))) +w("") +reg = CapabilityRegistry() +m = mint_capability(reg, CapabilityCandidate(raw_term="Incident Response"), category="Security") +ca = evaluate_relation(CapabilityRelation(relation_id="r1", source="certification:ISO27001", target_capability_id=m.capability_id, relationship_type=RelationType.SUPPORTS, evidence_kind=EvidenceKind.CERTIFICATION)) +cb = evaluate_relation(CapabilityRelation(relation_id="r2", source="ir-runbook.pdf", target_capability_id=m.capability_id, relationship_type=RelationType.CONFIRMS, evidence_kind=EvidenceKind.ARTIFACT)) +w("**Expected Master Capability Registry** _(computed confidence, policy-versioniert)_") +w("") +w("- ISO27001 *supports* %s -> %s/%s (policy %s) — computed, nicht gespeichert" % (m.capability_id, ca.status.value, ca.confidence.value, ca.policy_version)) +w("- ir-runbook.pdf *confirms* %s -> %s/%s — nur echtes Artefakt erreicht confirmed" % (m.capability_id, cb.status.value, cb.confidence.value)) +w("") +w("**Known Gaps:** `cap_*` (Company 2A) und `MCAP-*` (Registry) sind noch nicht verlinkt (RS-003).") +w("") +coverage_table([ + ("Company Context", "PASS", "ISO27001 + Declaration + Evidence"), + ("Trust-State (declared/inferred/confirmed)", "PASS", "Zertifizierung nie confirmed"), + ("Master Capability Registry", "PASS", "computed confidence, policy-versioniert"), + ("cap ↔ MCAP Linking", "TODO", "zwei Vokabulare unverbunden → RS-003"), +]) + +# ── Scenario 4 — Transition (RS-005 Planning Engine + Knowledge Pattern) ─── +w("## Szenario 4 — Transition (RS-005 fährt JEDEN Knowledge Pattern)") +w("") +w("_Genericity-Beweis: derselbe Algorithmus trägt jeden Transition Knowledge Pattern, nicht nur den CRA._") +w("") +_pat_dir = os.path.join(os.path.dirname(__file__), "..", "knowledge", "transition_patterns") +_pat_files = sorted(f for f in os.listdir(_pat_dir) + if f.startswith("transition_pattern_") and f.endswith(".yaml")) +_t_rows: List[Row] = [] +for _pf in _pat_files: + with open(os.path.join(_pat_dir, _pf), encoding="utf-8") as _f: + PAT = yaml.safe_load(_f) + if not isinstance(PAT["transition_goal"]["to"], dict): + continue # multi-target (Regulatory Convergence) patterns are handled in the convergence section + _src = "".join(c for c in PAT["transition_goal"]["from"]["standard"] if c.isalnum()) # e.g. ISOIEC27001 + _tgt = PAT["transition_goal"]["to"].get("regulation") or PAT["transition_goal"]["to"].get("framework") or "TARGET" + _have = [a["capability"] for a in PAT["likely_covered"]] + _map = {_src: CapabilityMappingEntry(capability_ids=_have, confidence=Confidence.MEDIUM)} + _profile = build_company_profile( + CompanyContext(company_id="kunde", certifications=[Certification(certification_id=_src)]), _map) + _reqs = [TargetRequirement(capability_id=a["capability"], question_intent="verify_existence", + expected_evidence=a.get("expected_evidence", [])) for a in PAT["likely_covered"]] + _reqs += [TargetRequirement(capability_id=d["capability"], question_intent=d.get("needed_information", "verify_existence"), + expected_evidence=d.get("expected_evidence", [])) for d in PAT["delta_requirements"]] + _tc = TransitionContext(company_id="kunde", known_certifications=[_src], + target=TransitionGoal(target_id=_tgt, target_type=TargetType.REGULATION, label=_tgt)) + _a = assess_transition(_tc, _reqs, _profile) + _carried = len(_a.coverage) == len(_reqs) and len(_a.question_requests) > 0 + _n_high = sum(1 for _r in _a.question_requests if _r.priority.value == "high") + w("**%s → %s** _(%s, status=%s)_" % (PAT["transition_goal"]["from"]["standard"], _tgt, PAT["id"], PAT["status"])) + w("> %s" % _a.summary.headline) + w("- Delta zuerst (HIGH): %s" % (", ".join(_r.capability_id for _r in _a.question_requests if _r.priority.value == "high") or "—")) + w("- vermutlich abgedeckt: %s" % (", ".join(_a.summary.probably_covered) or "—")) + w("- Pattern getragen: **%s** (%d caps → %d coverage + %d requests)" + % ("ja" if _carried else "NEIN", len(_reqs), len(_a.coverage), len(_a.question_requests))) + w("") + _t_rows.append(("Transition %s→%s" % (_src, _tgt), "PASS" if _carried else "PARTIAL", + "%s · %d HIGH-Delta + %d zu bestätigen" % (PAT["status"], _n_high, len(_a.summary.probably_covered)))) +_t_rows.append(("RS-005.1 Renderer (Fragetext)", "TODO", "verschoben — Engine liefert nur Requests")) +coverage_table(_t_rows) + +# ── Reference Transition Scenarios (RTS) — canonical regression (Soll/Ist + knowledge) ─── +w("## Reference Transition Scenarios (RTS) — kanonische Regression (Soll/Ist)") +w("") +w('_Anonymisierte Archetypen (KEINE Firmennamen). Jeder RTS pinnt ein Expected Outcome; jeder Commit muss es reproduzieren (identisch oder besser). Data Act = `uncertain`, nie fix „gilt/gilt-nicht"._') +w("") +_rts_dir = os.path.join(os.path.dirname(__file__), "..", "knowledge", "reference_transition_scenarios") +_pat_d2 = os.path.join(os.path.dirname(__file__), "..", "knowledge", "transition_patterns") +_pat_by_id = {} +for _pf in sorted(os.listdir(_pat_d2)): + if _pf.startswith("transition_pattern_") and _pf.endswith(".yaml"): + with open(os.path.join(_pat_d2, _pf), encoding="utf-8") as _h: + _pp = yaml.safe_load(_h) + _pat_by_id[_pp["id"]] = _pp +# Convergence pattern (multi-target) — run once through RS-005; reused by RTS-003 + the convergence section. +_CONV_ID = "TP-ISO27001-CRA-MaschinenVO-v1" +_MV_IDS = {"MaschinenVO", "MachineryRegulation", "Maschinenverordnung"} +CP = _pat_by_id.get(_CONV_ID) +_conv = None +_cp_missing = set() # type: ignore[var-annotated] +_all_t = [] # type: ignore[var-annotated] +_delta_t = {} # type: ignore[var-annotated] +if CP: + _all_t = sorted({t for it in CP["likely_covered"] + CP["delta_requirements"] for t in it.get("covers_targets", [])}) + _delta_t = {d["capability"]: d.get("covers_targets", []) for d in CP["delta_requirements"]} + _conv = regulatory_convergence(_delta_t, _all_t) + _cp_have = [a["capability"] for a in CP["likely_covered"]] + _cp_map = {"ISO27001": CapabilityMappingEntry(capability_ids=_cp_have, confidence=Confidence.MEDIUM)} + _cp_prof = build_company_profile( + CompanyContext(company_id="conv", certifications=[Certification(certification_id="ISO27001")]), _cp_map) + _cp_reqs = [TargetRequirement(capability_id=a["capability"]) for a in CP["likely_covered"]] + _cp_reqs += [TargetRequirement(capability_id=d["capability"], question_intent=d.get("needed_information", "verify_existence")) + for d in CP["delta_requirements"]] + _cp_a = assess_transition(TransitionContext(company_id="conv", target=TransitionGoal(target_id="CRA+MaschinenVO")), _cp_reqs, _cp_prof) + _cp_missing = {c.capability_id for c in _cp_a.coverage if c.status == CoverageStatus.MISSING} +_rts_rows: List[Row] = [] +for _rf in sorted(f for f in os.listdir(_rts_dir) if f.startswith("RTS-") and f.endswith(".yaml")): + with open(os.path.join(_rts_dir, _rf), encoding="utf-8") as _f: + RTS = yaml.safe_load(_f) + _cra = RTS["expected_outcome"]["cra"] + _pat = _pat_by_id[_cra["pattern"]] + _src = RTS["reference_company"]["known_certifications"][0] + _have = [a["capability"] for a in _pat["likely_covered"]] + _map = {_src: CapabilityMappingEntry(capability_ids=_have, confidence=Confidence.MEDIUM)} + _profile = build_company_profile( + CompanyContext(company_id="rts", certifications=[Certification(certification_id=_src)]), _map) + _reqs = [TargetRequirement(capability_id=a["capability"]) for a in _pat["likely_covered"]] + _reqs += [TargetRequirement(capability_id=d["capability"], question_intent=d.get("needed_information", "verify_existence")) + for d in _pat["delta_requirements"]] + _a = assess_transition(TransitionContext(company_id="rts", target=TransitionGoal(target_id="CRA")), _reqs, _profile) + _actual_missing = {c.capability_id for c in _a.coverage if c.status == CoverageStatus.MISSING} + _exp_delta = set(_cra["expected_delta_at_least"]) + _exp_cov = set(_cra["expected_likely_covered_at_least"]) + _delta_ok = _exp_delta <= _actual_missing + _cov_ok = _exp_cov <= set(_a.summary.probably_covered) + # Data Act: the engine must SURFACE uncertainty, never ASSERT "applies". + _tr = RTS["reference_company"]["product_traits"] + _P = P(name="rts", product_type=PT.MACHINERY if _tr.get("is_machine") else PT.HARDWARE, + markets=_tr.get("market", ["EU"]), economic_operator_role=Role.MANUFACTURER, + lifecycle_phase=LP.PLACING_ON_MARKET, is_machine=_tr.get("is_machine"), + is_component=_tr.get("is_component"), has_embedded_software=_tr.get("has_embedded_software"), + connected_to_internet=_tr.get("connected_to_internet"), has_remote_access=_tr.get("has_remote_access"), + generates_usage_data=_tr.get("generates_usage_data")) + _rmap = render_regulatory_map(_P) + _appl = {v.regulation_id for v in _rmap.applicable_regulations} + _unc = {v.regulation_id for v in _rmap.uncertain_regulations} + _da = "applicable" if "DataAct" in _appl else ("uncertain" if "DataAct" in _unc else "excluded/absent") + _da_ok = "DataAct" not in _appl # never wrongly asserted as applicable + _ok = _delta_ok and _cov_ok and _da_ok + w("**%s** — %s" % (RTS["id"], RTS["archetype"])) + w("> Start %s → CRA. %s" % ("+".join(RTS["reference_company"]["known_certifications"]), _a.summary.headline)) + w("- Expected Delta erfüllt: **%s** (%d/%d Soll-Delta in der Ist-Lücke)" % ("ja" if _delta_ok else "NEIN", len(_exp_delta & _actual_missing), len(_exp_delta))) + w("- Expected likely_covered erfüllt: **%s**" % ("ja" if _cov_ok else "NEIN")) + w("- Data Act: Engine sagt **%s** (Soll: uncertain; nie asserted) → %s" % (_da, "ok" if _da_ok else "FEHLER (asserted!)")) + # MaschinenVO — multi-regulation target. uncertain (component) vs applies (machine); convergence only with an ISMS. + _mv = RTS["expected_outcome"].get("maschinenvo") + _mv_ok = True + _mv_tag = "—" + if _mv and _mv.get("expectation") == "uncertain": + _mv_ok = not (_appl & _MV_IDS) # a component: never wrongly asserted as in-scope + _mv_tag = "uncertain(ok)" if _mv_ok else "uncertain(FEHLER:asserted)" + w("- MaschinenVO: Soll **uncertain** (Komponente, deciding: is_safety_component) → Engine asserted nicht: %s" % ("ok" if _mv_ok else "FEHLER")) + elif _mv: # expectation: applies (is_machine: true) + _mv_exp = set(_mv.get("expected_delta_at_least", [])) + if _mv.get("convergence_pattern") == _CONV_ID and CP: + _mv_ok = _mv_exp <= _cp_missing + _mv_tag = "applies %d/%d Safety-Delta" % (len(_mv_exp & _cp_missing), len(_mv_exp)) + w("- MaschinenVO **gilt** (is_machine): %d/%d Safety-Delta in der Ist-Lücke (Convergence-Pattern) → %s" % (len(_mv_exp & _cp_missing), len(_mv_exp), "ok" if _mv_ok else "NEIN")) + else: + _mv_ok = bool(_mv_exp) + _mv_tag = "applies (geringe Konvergenz, kein ISMS)" + w("- MaschinenVO **gilt** (is_machine): Safety-Delta %s — **geringe Konvergenz ohne ISMS** (RS-004 reg-map-Gate offen)" % (", ".join(sorted(_mv_exp)) or "—")) + # Convergence — the USP: capabilities covering CRA AND MaschinenVO at once. + _cv = RTS["expected_outcome"].get("convergence") + _cv_ok = True + if _cv and _conv: + _cv_exp = set(_cv.get("expected_multi_target_at_least", [])) + _cv_hit = _cv_exp & set(_conv.multi_target_capabilities) + _cv_ok = _cv_exp <= set(_conv.multi_target_capabilities) + w("- Konvergenz CRA∩MaschinenVO: %d/%d erwartete Multi-Target-Caps → %s (%s)" % (len(_cv_hit), len(_cv_exp), "ok" if _cv_ok else "NEIN", _conv.headline)) + _ok = _ok and _mv_ok and _cv_ok + w("") + _rts_rows.append(("%s (%s→CRA%s)" % (RTS["id"], _src, "+MaschVO" if _mv else ""), "PASS" if _ok else "PARTIAL", + "%d/%d Delta-Soll · likely_covered %s · DataAct=%s · MaschVO=%s%s" % ( + len(_exp_delta & _actual_missing), len(_exp_delta), "ok" if _cov_ok else "NEIN", _da, _mv_tag, + " · Konvergenz ok" if (_cv and _cv_ok) else ""))) +coverage_table(_rts_rows) + +# ── Regulatory Convergence — CRA + MaschinenVO (the multi-regulation USP) ─── +w("## Regulatory Convergence — CRA + MaschinenVO (Cross-Regulation Capability Mapping)") +w("") +w("_Der USP: welche Capability deckt MEHRERE Regelwerke gleichzeitig? (Convergence Pattern, RTS-003-Archetyp.)_") +w("") +assert _conv is not None and CP is not None # precomputed before the RTS loop (convergence pattern on disk) +w("**Cross-Regulation Capability Mapping (Delta):** %s" % _conv.headline) +w("") +w("**Konvergenz — diese neuen Maßnahmen decken BEIDE Regelwerke gleichzeitig:**") +for _c in _conv.multi_target_capabilities: + w("- `%s`" % _c) +w("") +w("**Pro Regelwerk benötigt (Delta):** " + ", ".join("%s=%d" % (k, v) for k, v in _conv.per_target_count.items())) +w("") +w('**Kundensatz:** „Von den %d neuen Maßnahmen erfüllen %d gleichzeitig CRA und MaschinenVO." (heute liefert das praktisch kein Tool)' + % (len(_delta_t), len(_conv.multi_target_capabilities))) +w("") +coverage_table([ + ("Regulatory Convergence Pattern", "PASS", "%d Targets, %d Delta-Capabilities" % (len(_all_t), len(_delta_t))), + ("Cross-Regulation Capability Mapping", "PASS", _conv.headline), +]) + +# ── Regulatory Optimization — Roadmap-Renderer über DEMSELBEN Capability Delta ─── +w("## Regulatory Optimization — größter regulatorischer Hebel zuerst") +w("") +w("_Dieselbe Berechnung wie die GAP-Analyse, anderer Renderer: das **Capability Delta** (RS-005) wird nach **regulatorischem Hebel** priorisiert (eine Maßnahme schließt N Regelwerke gleichzeitig). Welt-1: % über die IDENTIFIZIERTEN Anforderungen, kein Compliance-Urteil._") +w("") +_opt = roadmap_from_delta(_cp_a, _delta_t) # SAME delta the Interview Renderer turns into questions +_open_reqs = {_m.capability_id: _m.covers for _m in _opt.ranked_measures} +w("**Kompression:** %s" % _opt.headline) +w("") +w("**Top-Maßnahmen nach regulatorischem Hebel (Roadmap):**") +w("") +w("| # | Maßnahme | Hebel | deckt | kumuliert |") +w("|---|---|---|---|---|") +for _i, _m in enumerate(_opt.ranked_measures[:6], 1): + w("| %d | `%s` | **%d** | %s | %d/%d (%.0f%%) |" % ( + _i, _m.capability_id, _m.leverage, "+".join(_m.covers), + _m.cumulative_requirements, _opt.total_requirements, _m.cumulative_coverage * 100)) +w("") +_bud = select_within_budget(_open_reqs, 5) +w('**Managementsatz:** „Wenn Sie zuerst diese %d Maßnahmen umsetzen, schließen Sie %d von %d identifizierten Anforderungen (%.0f%%) — höchster regulatorischer Hebel." (Hebel skaliert mit jedem weiteren Regelwerk/Convergence-Pattern.)' + % (len(_bud.selected_capabilities), _bud.requirements_closed, _bud.total_requirements, _bud.coverage_ratio * 100)) +w("") +w("_Eine Wahrheit, zwei Renderer: dasselbe Capability Delta liefert dem Auditor **Fragen** (Interview) und dem GF **Maßnahmen** (Roadmap)._") +w("") +coverage_table([ + ("Capability Delta Engine (RS-005)", "PASS", "ein Delta, mehrere Renderer"), + ("Roadmap/Management Renderer (Hebel)", "PASS", _opt.headline), + ("Budget-Priorisierung", "PASS", "Top-5 → %.0f%% der identifizierten Anforderungen" % (_bud.coverage_ratio * 100)), +]) + +# ── Implementation Playbook — Berater-Renderer (wie komme ich dort hin?) ─── +w("## Implementation Playbook — wie komme ich dort hin? (Berater-Renderer)") +w("") +w('_Nach „was fehlt?" (Delta) und „womit anfangen?" (Hebel) die nächste Ebene: **wie umsetzen?** Pro Maßnahme eine komplette Reise aus kuratiertem Wissen + Hebel + (injizierten) Execution-Links. Inhalt ist der Engpass, nicht die Software._') +w("") +_pb_dir = os.path.join(os.path.dirname(__file__), "..", "knowledge", "implementation_playbooks") +_pb_kb = {} +for _pf in sorted(os.listdir(_pb_dir)): + if _pf.endswith(".yaml"): + with open(os.path.join(_pb_dir, _pf), encoding="utf-8") as _h: + _pd = yaml.safe_load(_h) + _pb_kb[_pd["capability_id"]] = _pd +_pbs = playbooks_for_plan(_opt, _pb_kb) # chain Roadmap -> Playbook over the SAME delta +_have = [p for p in _pbs if p.status != "missing"] +_miss = [p for p in _pbs if p.status == "missing"] +w("**Reise pro Maßnahme (aus der Roadmap):** %d von %d Maßnahmen haben ein Playbook; %d brauchen noch Inhalt (Knowledge Acquisition)." % (len(_have), len(_pbs), len(_miss))) +w("") +_show = next((p for p in _pbs if p.capability_id == "sbom_creation"), None) +if _show: + w("**Beispielreise — `%s`** _(%s, schließt %s)_" % (_show.capability_id, _show.status, "+".join(_show.closes_regulations) or "—")) + w("> **Warum?** %s" % _show.why.strip()) + w("- **Tools:** %s" % ", ".join(_show.tools)) + w("- **Prozess:** %s" % " → ".join(s.title for s in _show.process_steps)) + w("- **Nachweise:** %s" % ", ".join(_show.expected_evidence)) + w("- **Wie andere es tun:** %s" % _show.how_others_do_it.strip()) + w("") +w("**Roadmap → Implementation (Top-Maßnahmen nach Hebel):**") +w("") +w("| Maßnahme | Hebel | schließt | Playbook |") +w("|---|---|---|---|") +for _p in _pbs[:6]: + w("| `%s` | %d | %s | %s |" % (_p.capability_id, _p.leverage, "+".join(_p.closes_regulations) or "—", + ("✓ " + _p.status) if _p.status != "missing" else "**fehlt (Inhalt)**")) +w("") +w("_Derselbe Capability-Strang, neuer Renderer: aus Diagnose wird Beratung. Die `fehlt`-Einträge sind der ehrliche Content-Backlog (höchster Hebel zuerst befüllen)._") +w("") +coverage_table([ + ("Implementation Playbook Renderer", "PASS", "Reise pro Capability (why/tools/process/evidence/controls)"), + ("Roadmap → Playbook (Verkettung)", "PASS", "%d/%d Maßnahmen mit Playbook" % (len(_have), len(_pbs))), + ("Playbook-Inhalt (Knowledge)", "TODO" if _miss else "PASS", "%d Capabilities brauchen noch Inhalt" % len(_miss)), +]) + +# ── Knowledge Production — Playbook Draft Generator (vorbereiten, dann kuratieren) ─── +w("## Knowledge Production — Playbook-Entwürfe automatisch assemblieren") +w("") +w("_Der Engpass ist nicht Content, sondern Wissensproduktion. Der Corpus wird nicht von Hand geschrieben, sondern deterministisch aus vorhandenen Daten (Transition Pattern + Leverage + injizierte Controls) vorbereitet — dann fachlich kuratiert (wie Gesetz→Parser→Obligation→Review)._") +w("") +_kp = drafts_from_pattern(CP) if CP else [] # CP = convergence pattern (already loaded) +w("**Aus 1 Pattern → %d Playbook-Entwürfe** (`status: draft_generated`): eigene Felder (Warum/schließt/Nachweise) aus den Daten gefüllt, der Experte ergänzt nur Tools/Prozess/How-others." % len(_kp)) +w("") +_kd = next((d for d in _kp if d.capability_id == "sbom_creation"), _kp[0] if _kp else None) +if _kd: + w("**Beispiel-Entwurf — `%s`** _(%s)_" % (_kd.capability_id, _kd.status.value)) + w("- **Warum** (aus Pattern): %s" % _kd.why.strip()) + w("- **schließt** %s · **Nachweise** %s" % ("+".join(_kd.closes_regulations) or "—", ", ".join(_kd.expected_evidence) or "—")) + w("- **Provenance:** %s" % ", ".join("%s←%s" % (k, v) for k, v in _kd.provenance.items())) + w("- **TODO (Experte/Offline-Propose):** %s" % ", ".join(_kd.todo)) + w("") +w("_So reviewt der Experte %d Entwürfe statt %d Playbooks zu schreiben. Derselbe Generator bereitet später ISO14001-/IATF-Entwürfe vor, sobald der Corpus da ist._" % (len(_kp), len(_kp))) +w("") +coverage_table([ + ("Playbook Draft Generator (deterministisch)", "PASS", "%d Entwürfe aus 1 Pattern, kein LLM im Kern" % len(_kp)), + ("Provenance + TODO + Freigabestatus", "PASS", "draft_generated→reviewed→validated→proven"), + ("Draft-Generatoren neue Domänen (Phase A)", "TODO", "Transition-/Reference-Scenario-Drafts"), +]) + +knowledge_intake_section(os.path.dirname(__file__)) # Knowledge Intake (impact triage) — kept in _helpers for LOC +completeness_section() # Regulatory Completeness — kept in _helpers for LOC +domain_programs_section(os.path.dirname(__file__)) # Domain Knowledge Programs — kept in _helpers for LOC +transition_coverage_section(os.path.dirname(__file__)) # Transition Coverage (Operational Knowledge) — in _helpers for LOC + +# ── Epics + roll-up ─────────────────────────────────────────────────────── +w("## Gaps → Epics (Backlog — nur erfasst, NICHT implementiert)") +w("") +w("| Epic | Titel | schliesst Coverage-Luecke |") +w("|---|---|---|") +w("| RS-001 | Interpretation Pattern Library | Sz1 Interpretation PARTIAL -> PASS (CRA-Muster) |") +w("| RS-002 | Environmental Corpus (Pilotdomaene) | Sz2 Environmental UNSUPPORTED -> PASS |") +w("| RS-003 | Capability Linking (cap↔MCAP) + Company-Gap | Sz1/Sz3 Company Gap TODO -> PASS |") +w("| RS-004 | MaschinenVO/EMV Registry Linking | Sz1/Sz2 MaschinenVO/EMV PARTIAL -> PASS |") +w("") +total = len(ROLLUP) +npass = ROLLUP.count("PASS") +w("## Suite-Status (Roll-up)") +w("") +w("- Coverage-Zellen gesamt: **%d**" % total) +w("- PASS: **%d** · PARTIAL: %d · UNSUPPORTED: %d · TODO: %d · N/A: %d · NEEDS_FACTS: %d" + % (npass, ROLLUP.count("PARTIAL"), ROLLUP.count("UNSUPPORTED"), ROLLUP.count("TODO"), + ROLLUP.count("N/A"), ROLLUP.count("NEEDS_FACTS"))) +w("- Fortschritt = PASS-Anteil steigt, wenn Epics RS-001…004 landen (objektiver Maßstab, kein LOC).") + +print("\n".join(OUT)) diff --git a/backend-compliance/reference_scenarios/journey_matcher_demo.md b/backend-compliance/reference_scenarios/journey_matcher_demo.md new file mode 100644 index 00000000..ffaab49e --- /dev/null +++ b/backend-compliance/reference_scenarios/journey_matcher_demo.md @@ -0,0 +1,28 @@ +# Journey Matcher — Delta -> Journey (an echten Pattern validiert) + +_Der Matcher fragt NICHT „welche Journey passt?", sondern „welche bekannten Journeys ERKLÄREN dieses Capability Delta?". Er sieht nur das Delta — keine Zertifikate, kein Regelwerk, kein Ziel. Journey = Erklärung, nicht Ursache. Deterministisch, kein ML/Embedding/LLM. Synthetischer Kunde, keine echten Namen._ + +## Eingang: ein echtes Capability Delta +- Multi-zertifiziertes Unternehmen will **CRA + MaschinenVO** → **9 fehlende Capabilities** (aus RS-005). +- Der Matcher bekommt **nur diese 9 Capabilities** — sonst nichts. + +## Delta -> Journey: Rangliste (Anteil des Deltas, den die Journey erklärt) +> 3 Journeys erklaeren das Delta; beste: ISO27001 -> CRA + MaschinenVO (100% des Deltas) + +| Journey (Capability-Cluster) | erklärt | Anteil | +|---|---|---| +| **ISO27001 -> CRA + MaschinenVO** | 9 von 9 fehlenden Capabilities | 100% | +| **ISO27001 -> CRA** | 5 von 9 fehlenden Capabilities | 56% | +| **ISO9001 -> CRA** | 4 von 9 fehlenden Capabilities | 44% | +| **ISMS -> TISAX** | 0 von 9 fehlenden Capabilities | 0% | + +## Warum „ISO27001 -> CRA + MaschinenVO"? — auditierbar, keine Blackbox +- **Erklärte Capabilities (9):** `machine_safety_risk_assessment`, `mechanical_safety_and_guards`, `operating_instructions_and_safety_information`, `product_cyber_risk_assessment`, `protection_against_corruption_of_safety_functions`, `public_security_advisories` … +- **Nicht erklärt (Rest-Delta):** — (Journey erklärt das GESAMTE Delta) +- **Journey reicht darüber hinaus:** `ce_conformity_assessment_and_technical_documentation`, `coordinated_vulnerability_disclosure`, `exploited_vuln_and_incident_reporting` +- **Kontext-Signale:** gleiche Zielart + +## Der Paradigmenwechsel + +> Reihenfolge ist jetzt **`Goal → Required → Delta → Journey`**, nicht mehr `Goal → Journey → Delta`. Die Journey ist die **Erklärung** des Deltas. Der Matcher ist bewusst **dumm + deterministisch** (reine Mengenüberlappung) und damit auditierbar; ein lernendes Ranking kann später DAVOR gesetzt werden. Drei austauschbare Funktionen: `Evidence→Capability` (Company 2A) · `Capability→Delta` (RS-005) · **`Delta→Journey` (dieser Matcher)**. In keiner kommt „Regulation" als Sonderfall vor — CRA, TISAX, Ausschreibung, OEM-Spec und Umweltziel sind nur verschiedene Quellen des Required State. + diff --git a/backend-compliance/reference_scenarios/journey_matcher_demo.py b/backend-compliance/reference_scenarios/journey_matcher_demo.py new file mode 100644 index 00000000..c9065d72 --- /dev/null +++ b/backend-compliance/reference_scenarios/journey_matcher_demo.py @@ -0,0 +1,108 @@ +# ruff: noqa +# mypy: ignore-errors +"""Journey Matcher demo — Delta -> Journey on the REAL transition patterns. + +Validates the new matcher end-to-end: take a real Capability Delta (a multi-certified company that +wants CRA + MaschinenVO), then rank the KNOWN journeys purely by how much of THAT delta each explains. +The matcher never looks at the certificates, the regulation or the goal — only at the delta. The +journey is the EXPLANATION of the delta, not its cause (order: Goal -> Required -> Delta -> Journey). + +Journey signatures are derived from the transition-pattern YAMLs here (non-core), then injected into the +hermetic engine. Synthetic company (NO real names). Non-runtime -> no deploy. +Run: cd backend-compliance && PYTHONPATH=. python3 reference_scenarios/journey_matcher_demo.py +""" +from __future__ import annotations + +import os +import yaml + +from compliance.company import ( + CompanyContext, Certification, CapabilityMappingEntry, build_company_profile, +) +from compliance.reasoning.enums import Confidence +from compliance.transition_reasoning import ( + TransitionContext, TransitionGoal, TargetRequirement, assess_transition, CoverageStatus, +) +from compliance.journey_matcher import JourneySignature, MatchContext, match_journeys + +OUT = [] + + +def w(s=""): + OUT.append(s) + + +_K = os.path.join(os.path.dirname(__file__), "..", "knowledge", "transition_patterns") +_PATTERNS = { + "transition_pattern_iso27001_to_cra_maschinenvo_v1.yaml": ("ISO27001 -> CRA + MaschinenVO", "regulation"), + "transition_pattern_iso27001_to_cra_v1.yaml": ("ISO27001 -> CRA", "regulation"), + "transition_pattern_iso9001_to_cra_v1.yaml": ("ISO9001 -> CRA", "regulation"), + "transition_pattern_isms_to_tisax_v1.yaml": ("ISMS -> TISAX", "certification"), +} + + +def _load(name): + return yaml.safe_load(open(os.path.join(_K, name), encoding="utf-8")) + + +# ── Journey library: signatures = capability CLUSTERS (the matcher never reads the IDs) ────── +journeys = [] +for fname, (label, ttype) in _PATTERNS.items(): + p = _load(fname) + journeys.append(JourneySignature( + journey_id=p.get("id", fname), + label=label, + capability_pattern=[d["capability"] for d in p["delta_requirements"]], # OUTPUT cluster + assumed_capabilities=[a["capability"] for a in p["likely_covered"]], # INPUT cluster + target_type=ttype, + )) + +# ── A real Capability Delta: multi-certified company that wants CRA + MaschinenVO ──────────── +CP = _load("transition_pattern_iso27001_to_cra_maschinenvo_v1.yaml") +infosec = [a["capability"] for a in CP["likely_covered"]] +cmap = { + "ISO27001": CapabilityMappingEntry(capability_ids=infosec, confidence=Confidence.MEDIUM), + "PSIRT": CapabilityMappingEntry(capability_ids=["coordinated_vulnerability_disclosure", + "exploited_vuln_and_incident_reporting"], confidence=Confidence.HIGH), + "ISO9001": CapabilityMappingEntry(capability_ids=["ce_conformity_assessment_and_technical_documentation"], + confidence=Confidence.MEDIUM), +} +profile = build_company_profile( + CompanyContext(company_id="d", certifications=[Certification(certification_id=k) for k in cmap]), cmap) +reqs = [TargetRequirement(capability_id=a["capability"]) for a in CP["likely_covered"]] +reqs += [TargetRequirement(capability_id=d["capability"]) for d in CP["delta_requirements"]] +assess = assess_transition(TransitionContext(company_id="d", target=TransitionGoal(target_id="CRA+MaschinenVO")), reqs, profile) +delta = sorted({c.capability_id for c in assess.coverage if c.status == CoverageStatus.MISSING}) + +# ── Delta -> Journey: rank the known journeys that EXPLAIN this delta ──────────────────────── +result = match_journeys(delta, journeys, MatchContext(target_type="regulation")) + +w("# Journey Matcher — Delta -> Journey (an echten Pattern validiert)") +w("") +w('_Der Matcher fragt NICHT „welche Journey passt?", sondern „welche bekannten Journeys ERKLÄREN dieses Capability Delta?". Er sieht nur das Delta — keine Zertifikate, kein Regelwerk, kein Ziel. Journey = Erklärung, nicht Ursache. Deterministisch, kein ML/Embedding/LLM. Synthetischer Kunde, keine echten Namen._') +w("") +w("## Eingang: ein echtes Capability Delta") +w("- Multi-zertifiziertes Unternehmen will **CRA + MaschinenVO** → **%d fehlende Capabilities** (aus RS-005)." % len(delta)) +w("- Der Matcher bekommt **nur diese %d Capabilities** — sonst nichts." % len(delta)) +w("") +w("## Delta -> Journey: Rangliste (Anteil des Deltas, den die Journey erklärt)") +w("> %s" % result.headline) +w("") +w("| Journey (Capability-Cluster) | erklärt | Anteil |") +w("|---|---|---|") +for m in result.matches: + w("| **%s** | %s | %d%% |" % (m.label, m.explains, round(m.score * 100))) +w("") +b = result.best +w('## Warum „%s"? — auditierbar, keine Blackbox' % b.label) +w("- **Erklärte Capabilities (%d):** %s" % (len(b.reason.matched_capabilities), ", ".join("`%s`" % c for c in b.reason.matched_capabilities[:6]) + (" …" if len(b.reason.matched_capabilities) > 6 else ""))) +w("- **Nicht erklärt (Rest-Delta):** %s" % (", ".join("`%s`" % c for c in b.reason.unexplained_delta) or "— (Journey erklärt das GESAMTE Delta)")) +w("- **Journey reicht darüber hinaus:** %s" % (", ".join("`%s`" % c for c in b.reason.journey_only) or "—")) +w("- **Kontext-Signale:** %s" % (", ".join(b.reason.context_signals) or "—")) +w("") +w("## Der Paradigmenwechsel") +w("") +w('> Reihenfolge ist jetzt **`Goal → Required → Delta → Journey`**, nicht mehr `Goal → Journey → Delta`. Die Journey ist die **Erklärung** des Deltas. Der Matcher ist bewusst **dumm + deterministisch** (reine Mengenüberlappung) und damit auditierbar; ein lernendes Ranking kann später DAVOR gesetzt werden. Drei austauschbare Funktionen: `Evidence→Capability` (Company 2A) · `Capability→Delta` (RS-005) · **`Delta→Journey` (dieser Matcher)**. In keiner kommt „Regulation" als Sonderfall vor — CRA, TISAX, Ausschreibung, OEM-Spec und Umweltziel sind nur verschiedene Quellen des Required State.') +w("") + +print("\n".join(OUT)) diff --git a/backend-compliance/reference_scenarios/mcap_convergence_analysis.md b/backend-compliance/reference_scenarios/mcap_convergence_analysis.md new file mode 100644 index 00000000..18a8a402 --- /dev/null +++ b/backend-compliance/reference_scenarios/mcap_convergence_analysis.md @@ -0,0 +1,59 @@ +# Cross-Domain MCAP Convergence Analysis — wo konvergiert das Wissensmodell? + +_Nicht „welche MCAPs kommen am häufigsten vor?" (Häufigkeit täuscht), sondern „welche MCAPs TRAGEN den größten Teil des Systems?". Deterministischer **Impact-Score** (kein ML), internes Engineering-Werkzeug, reine Aggregation über vorhandene Daten (6 Transition Patterns inkl. Medical + 7 Automotive-Quellen). Non-runtime, keine echten Namen._ + +## Impact-Score (deterministisch) +> `Impact = distinct Sources + distinct Target-Types + distinct Domains + distinct Journeys + Regulatory Leverage + Business Leverage` +- 75 distinct Capabilities (MCAP-Kandidaten) über alle Quellen aggregiert. + +## 1. Core MCAPs — höchster Impact (die tragenden Knoten) +| Capability | Impact | Sources | Types | Domains | Journeys | +|---|---:|---:|---:|---:|---:| +| `secure_signed_update_distribution` | **24** | 7 | 2 | 3 | 5 | +| `technical_vulnerability_management` | **23** | 7 | 3 | 3 | 5 | +| `access_control_and_authentication` | **19** | 5 | 2 | 3 | 6 | +| `incident_management` | **14** | 4 | 2 | 2 | 4 | +| `product_cyber_risk_assessment` | **13** | 3 | 1 | 2 | 4 | +| `sbom_creation` | **13** | 2 | 1 | 3 | 5 | +| `secure_development_lifecycle` | **11** | 2 | 2 | 2 | 4 | +| `supplier_security` | **11** | 3 | 2 | 2 | 3 | +| `ce_conformity_assessment_and_technical_documentation` | **9** | 2 | 1 | 1 | 3 | +| `coordinated_vulnerability_disclosure` | **9** | 1 | 1 | 2 | 4 | + +→ Hoher Impact = ein Knoten verbindet viele Quellen ÜBER Typen/Domänen/Journeys hinweg — nicht „in 40 Dokumenten einer Normenfamilie". + +## 2. Emerging MCAPs — verbinden ≥2 Domänen (Brücken zwischen Anforderungswelten) +- `secure_signed_update_distribution` — 3 Domänen (automotive, industrial_automation, medical), 2 Typen. +- `technical_vulnerability_management` — 3 Domänen (automotive, industrial_automation, medical), 3 Typen. +- `access_control_and_authentication` — 3 Domänen (automotive, industrial_automation, medical), 2 Typen. +- `incident_management` — 2 Domänen (automotive, industrial_automation), 2 Typen. +- `product_cyber_risk_assessment` — 2 Domänen (automotive, industrial_automation), 1 Typen. +- `sbom_creation` — 3 Domänen (automotive, industrial_automation, medical), 1 Typen. +- `secure_development_lifecycle` — 2 Domänen (automotive, industrial_automation), 2 Typen. +- `supplier_security` — 2 Domänen (automotive, industrial_automation), 2 Typen. +- _(Echtes „Wachstum über Zeit" braucht historische Snapshots — hier Proxy = Domänen-Spannweite jetzt.)_ + +## 3. Isolated MCAPs — nur 1 Quelle/Journey (Review: spezialisiert ODER Konvergenz übersehen?) +- 47 Stück, u. a.: `account_energy_consumption`, `assign_unique_device_identification`, `classify_software_safety_iec62304`, `compile_medical_technical_documentation`, `conduct_clinical_evaluation`, `cybersecurity_management_system`, `document_update_campaigns`, `document_waste_streams`. + +## 4. Suspicious MCAPs — Abstraktionsgrad-Verdacht (Experten-Review) +- **Evtl. zu grob** (generisches Verb, breit aber nur 1 Typ): `document_and_change_control`, `manage_chemical_substances`. +- **Evtl. zu fein** (isoliert + sehr spezifischer Name): `assign_unique_device_identification`, `compile_medical_technical_documentation`, `implement_software_lifecycle_iec62304`, `operating_instructions_and_safety_information`, `provide_dedicated_security_contact`, `provide_functional_safety_evidence`. +- Die Analyse sagt damit nicht nur WELCHE MCAPs wichtig sind, sondern auch, ob sie auf dem **richtigen Abstraktionsniveau** definiert sind. + +## 5. Missing Convergence — mögliche strukturelle Doppelungen (Experten-Review, KEIN Auto-Merge) +> Nicht „welche MCAPs existieren?", sondern „welche hätte ich aufgrund der Daten ERWARTET, existieren aber als GETRENNTE MCAPs?". Deterministische Heuristik (kein ML): geteiltes Namens-Token über ≥3 MCAPs UND ≥2 verschiedene Quellen = Konvergenz-Kandidat (eine Einzelquelle-Zerlegung zählt NICHT). Nur anzeigen, nie automatisch zusammenführen. + +- **Token `secure` → 3 MCAPs / 8 Quellen:** `secure_by_default_no_default_credentials`, `secure_development_lifecycle`, `secure_signed_update_distribution` — _Review: eine Fähigkeit in mehreren Facetten?_ +- **Token `update` → 4 MCAPs / 7 Quellen:** `document_update_campaigns`, `secure_signed_update_distribution`, `security_update_support_period`, `software_update_management_system` — _Review: eine Fähigkeit in mehreren Facetten?_ +- **Token `risk` → 6 MCAPs / 6 Quellen:** `machine_safety_risk_assessment`, `maintain_risk_management_file_iso14971`, `perform_benefit_risk_analysis`, `product_cyber_risk_assessment`, `run_risk_management_process`, `threat_analysis_and_risk_assessment` — _Review: eine Fähigkeit in mehreren Facetten?_ +- **Token `document` → 5 MCAPs / 6 Quellen:** `document_and_change_control`, `document_update_campaigns`, `document_waste_streams`, `manage_document_control`, `treat_and_document_wastewater` — _Review: eine Fähigkeit in mehreren Facetten?_ +- **Token `security` → 8 MCAPs / 4 Quellen:** `information_security_management`, `physical_security`, `provide_dedicated_security_contact`, `public_security_advisories`, `security_awareness_training`, `security_logging_and_monitoring`, `security_update_support_period`, `supplier_security` — _Review: eine Fähigkeit in mehreren Facetten?_ +- **Token `safety` → 6 MCAPs / 4 Quellen:** `classify_software_safety_iec62304`, `machine_safety_risk_assessment`, `mechanical_safety_and_guards`, `operating_instructions_and_safety_information`, `protection_against_corruption_of_safety_functions`, `provide_functional_safety_evidence` — _Review: eine Fähigkeit in mehreren Facetten?_ + +→ Ergänzt `Suspicious`: dieser Report schaut nicht auf einzelne MCAPs, sondern auf **strukturelle Doppelungen** über Quellen hinweg — der häufigste systematische Modellierungsfehler. + +## Befund + +> **Ein Kern beginnt sich zu zeigen:** 11 von 75 Capabilities erreichen Impact ≥ 8 (tragende Knoten), 14 verbinden ≥2 Domänen. Mit Medical (6 Patterns inkl. ISO 13485 + Automotive) zeigt sich erstmals die **Safety/Security-Kopplung als Capability-REUSE**: IEC 81001-5-1 zieht dieselben Security-MCAPs wie die CRA herein → diese Knoten spannen jetzt Cyber + Maschinenbau + Automotive + Medizin. Die Methode steht; sobald Payment/weitere Domänen als DATEN kommen, zeigt dieselbe Aggregation (+ der neue Missing-Convergence-Report), ob sich der erwartete stabile Kern von 30–50 hochkonvergenten MCAPs bildet — der gemeinsame Strukturkern hinter sehr unterschiedlichen Anforderungswelten. Tieferer Wertnachweis als „eine weitere Norm unterstützt". Reine Aggregation, 0 Runtime, 0 neue Architektur. + diff --git a/backend-compliance/reference_scenarios/mcap_convergence_analysis.py b/backend-compliance/reference_scenarios/mcap_convergence_analysis.py new file mode 100644 index 00000000..b4974451 --- /dev/null +++ b/backend-compliance/reference_scenarios/mcap_convergence_analysis.py @@ -0,0 +1,178 @@ +# ruff: noqa +# mypy: ignore-errors +"""Cross-Domain MCAP Convergence Analysis — where does the knowledge model converge? (Phase Ω, pause) + +After Automotive the user paused on adding domains to ask a deeper question. NOT "which MCAPs occur most +often?" (frequency deceives — a generic `document_changes` may be in 40 sources but is not the product +core) but "which MCAPs CARRY the largest part of the system?". The answer is a deterministic MCAP IMPACT +SCORE (no AI), an internal engineering tool, computed by aggregating over the EXISTING data only. + + Impact(MCAP) = distinct Requirement Sources + distinct Target Types + distinct Domains + + distinct Journeys + Regulatory Leverage + Business Leverage + +Four reports, all pure aggregation (no new runtime, no new architecture): + 1. Core — highest impact (the cross-cutting nodes that carry the system) + 2. Emerging — span >= 2 domains (bridges across requirement worlds) + 3. Isolated — only one source/journey/domain (specialised, OR convergence not yet recognised) + 4. Suspicious— probably cut too coarse (generic) or too fine (one hyper-specific occurrence) + +Non-runtime -> no deploy. +Run: cd backend-compliance && PYTHONPATH=. python3 reference_scenarios/mcap_convergence_analysis.py +""" +from __future__ import annotations + +import os +import yaml + +OUT = [] + + +def w(s=""): + OUT.append(s) + + +_HERE = os.path.dirname(__file__) +_TP = os.path.join(_HERE, "..", "knowledge", "transition_patterns") + +# pattern -> (domain, default target_type, default sources, source_type-of-default) +PATTERN_META = { + "transition_pattern_iso27001_to_cra_maschinenvo_v1.yaml": ("industrial_automation", "regulation", ["CRA", "MaschinenVO"]), + "transition_pattern_iso27001_to_cra_v1.yaml": ("industrial_automation", "regulation", ["CRA"]), + "transition_pattern_iso9001_to_cra_v1.yaml": ("industrial_automation", "regulation", ["CRA"]), + "transition_pattern_isms_to_tisax_v1.yaml": ("automotive", "certification", ["TISAX"]), + "transition_pattern_iso14001_to_environmental_v1.yaml": ("environmental", "regulation", + ["REACH", "RoHS", "Batterieverordnung", "Wasserrecht", "Abwasservorschriften", "Energiemanagement", "Kreislaufwirtschaft", "Emissionsschutz"]), + "transition_pattern_iso13485_to_medical_v1.yaml": ("medical", "regulation", + ["MDR", "IEC_62304", "ISO_14971", "IEC_81001_5_1"]), +} + +# capability -> dict of sets we aggregate +idx = {} + + +def _ent(cap): + return idx.setdefault(cap, {"sources": set(), "types": set(), "domains": set(), "journeys": set(), + "regs": set(), "markets": set()}) + + +def _add(cap, sources, stype, domain, journey): + e = _ent(cap) + e["sources"] |= set(sources) + e["types"].add(stype) + e["domains"].add(domain) + e["journeys"].add(journey) + if stype == "regulation": + e["regs"] |= set(sources) + if stype == "contract": + e["markets"] |= set(sources) + + +# ── A) transition patterns: each pattern is a journey with a target/domain ─────────────────── +# IMPORTANT (anti-frequency-deception): a `likely_covered` cap is PROVIDED BY the source cert (one +# certification source), NOT required by every target regulation — attributing all target sources to it +# would inflate management caps on raw frequency alone. Only `delta` caps name their real target sources. +for fname, (domain, ttype, default_sources) in PATTERN_META.items(): + p = yaml.safe_load(open(os.path.join(_TP, fname), encoding="utf-8")) + journey = p.get("id", fname) + cert = (p.get("transition_goal", {}).get("from", {}) or {}).get("standard", journey) + for a in p.get("likely_covered", []): + _add(a["capability"], [cert], "certification", domain, journey) # provided by the cert + for d in p.get("delta_requirements", []): + srcs = d.get("covers_targets") or default_sources # required by these target sources + _add(d["capability"], srcs, ttype, domain, journey) + +# ── B) automotive multi-source data: precise per-source attribution ────────────────────────── +A = yaml.safe_load(open(os.path.join(_HERE, "..", "knowledge", "domains", "automotive", "source_capabilities.yaml"), encoding="utf-8")) +for s in A["sources"]: + for cap in s["requires"]: + _add(cap, [s["id"]], s["type"], "automotive", "automotive_ecu") + +# ── Impact score (deterministic) ───────────────────────────────────────────────────────────── +def impact(e): + return len(e["sources"]) + len(e["types"]) + len(e["domains"]) + len(e["journeys"]) + len(e["regs"]) + len(e["markets"]) + + +scored = sorted(idx.items(), key=lambda kv: (-impact(kv[1]), kv[0])) +GENERIC = ("document_", "manage_", "control_", "conduct_", "operate_", "run_", "assign_", "plan_", "approve_") + +w("# Cross-Domain MCAP Convergence Analysis — wo konvergiert das Wissensmodell?") +w("") +w('_Nicht „welche MCAPs kommen am häufigsten vor?" (Häufigkeit täuscht), sondern „welche MCAPs TRAGEN den größten Teil des Systems?". Deterministischer **Impact-Score** (kein ML), internes Engineering-Werkzeug, reine Aggregation über vorhandene Daten (6 Transition Patterns inkl. Medical + 7 Automotive-Quellen). Non-runtime, keine echten Namen._') +w("") +w("## Impact-Score (deterministisch)") +w("> `Impact = distinct Sources + distinct Target-Types + distinct Domains + distinct Journeys + Regulatory Leverage + Business Leverage`") +w("- %d distinct Capabilities (MCAP-Kandidaten) über alle Quellen aggregiert." % len(idx)) +w("") + +# ── 1. Core MCAPs ───────────────────────────────────────────────────────── +w("## 1. Core MCAPs — höchster Impact (die tragenden Knoten)") +w("| Capability | Impact | Sources | Types | Domains | Journeys |") +w("|---|---:|---:|---:|---:|---:|") +for cap, e in scored[:10]: + w("| `%s` | **%d** | %d | %d | %d | %d |" % (cap, impact(e), len(e["sources"]), len(e["types"]), len(e["domains"]), len(e["journeys"]))) +w("") +w('→ Hoher Impact = ein Knoten verbindet viele Quellen ÜBER Typen/Domänen/Journeys hinweg — nicht „in 40 Dokumenten einer Normenfamilie".') +w("") + +# ── 2. Emerging MCAPs (cross-domain bridges) ────────────────────────────── +emerging = [(c, e) for c, e in scored if len(e["domains"]) >= 2] +w("## 2. Emerging MCAPs — verbinden ≥2 Domänen (Brücken zwischen Anforderungswelten)") +for cap, e in emerging[:8]: + w("- `%s` — %d Domänen (%s), %d Typen." % (cap, len(e["domains"]), ", ".join(sorted(e["domains"])), len(e["types"]))) +w('- _(Echtes „Wachstum über Zeit" braucht historische Snapshots — hier Proxy = Domänen-Spannweite jetzt.)_') +w("") + +# ── 3. Isolated MCAPs ───────────────────────────────────────────────────── +isolated = [(c, e) for c, e in scored if len(e["sources"]) == 1 and len(e["journeys"]) == 1] +w("## 3. Isolated MCAPs — nur 1 Quelle/Journey (Review: spezialisiert ODER Konvergenz übersehen?)") +w("- %d Stück, u. a.: %s." % (len(isolated), ", ".join("`%s`" % c for c, _ in isolated[:8]))) +w("") + +# ── 4. Suspicious MCAPs (abstraction level) ─────────────────────────────── +too_coarse = [(c, e) for c, e in scored if c.startswith(GENERIC) and len(e["types"]) <= 1 and len(e["sources"]) >= 2] +too_fine = [(c, e) for c, e in isolated if len(c) >= 34] +w("## 4. Suspicious MCAPs — Abstraktionsgrad-Verdacht (Experten-Review)") +w("- **Evtl. zu grob** (generisches Verb, breit aber nur 1 Typ): %s." % (", ".join("`%s`" % c for c, _ in too_coarse[:6]) or "—")) +w("- **Evtl. zu fein** (isoliert + sehr spezifischer Name): %s." % (", ".join("`%s`" % c for c, _ in too_fine[:6]) or "—")) +w("- Die Analyse sagt damit nicht nur WELCHE MCAPs wichtig sind, sondern auch, ob sie auf dem **richtigen Abstraktionsniveau** definiert sind.") +w("") + +# ── 5. Missing Convergence — potential structural duplications (token clusters) ────────────── +STOP = {"and", "of", "the", "for", "to", "a", "an", "no", "on", "in", "by", "with", "per"} +GENERIC_TOK = {"management", "process", "control", "distribution", "documentation", "technical", + "internal", "continual", "assessment", "evidence"} +tok2caps = {} +for cap in idx: + for t in cap.split("_"): + if len(t) >= 4 and t not in STOP and t not in GENERIC_TOK: + tok2caps.setdefault(t, set()).add(cap) +# cross-source signal only: a token shared by >=3 MCAPs that span >=2 distinct sources (one source's +# own decomposition is NOT a duplication candidate — that is intended granularity). +clusters = [] +for t, cs in tok2caps.items(): + if len(cs) >= 3: + srcs = set() + for c in cs: + srcs |= idx[c]["sources"] + if len(srcs) >= 2: + clusters.append((t, sorted(cs), len(srcs))) +clusters.sort(key=lambda x: (-x[2], -len(x[1]), x[0])) +w("## 5. Missing Convergence — mögliche strukturelle Doppelungen (Experten-Review, KEIN Auto-Merge)") +w('> Nicht „welche MCAPs existieren?", sondern „welche hätte ich aufgrund der Daten ERWARTET, existieren aber als GETRENNTE MCAPs?". Deterministische Heuristik (kein ML): geteiltes Namens-Token über ≥3 MCAPs UND ≥2 verschiedene Quellen = Konvergenz-Kandidat (eine Einzelquelle-Zerlegung zählt NICHT). Nur anzeigen, nie automatisch zusammenführen.') +w("") +for t, cs, ns in clusters[:6]: + w("- **Token `%s` → %d MCAPs / %d Quellen:** %s — _Review: eine Fähigkeit in mehreren Facetten?_" % ( + t, len(cs), ns, ", ".join("`%s`" % c for c in cs))) +w("") +w("→ Ergänzt `Suspicious`: dieser Report schaut nicht auf einzelne MCAPs, sondern auf **strukturelle Doppelungen** über Quellen hinweg — der häufigste systematische Modellierungsfehler.") +w("") + +# ── Befund ───────────────────────────────────────────────────────────────── +core_cut = [c for c, e in scored if impact(e) >= 8] +cross = [c for c, e in scored if len(e["domains"]) >= 2] +w("## Befund") +w("") +w('> **Ein Kern beginnt sich zu zeigen:** %d von %d Capabilities erreichen Impact ≥ 8 (tragende Knoten), %d verbinden ≥2 Domänen. Mit Medical (6 Patterns inkl. ISO 13485 + Automotive) zeigt sich erstmals die **Safety/Security-Kopplung als Capability-REUSE**: IEC 81001-5-1 zieht dieselben Security-MCAPs wie die CRA herein → diese Knoten spannen jetzt Cyber + Maschinenbau + Automotive + Medizin. Die Methode steht; sobald Payment/weitere Domänen als DATEN kommen, zeigt dieselbe Aggregation (+ der neue Missing-Convergence-Report), ob sich der erwartete stabile Kern von 30–50 hochkonvergenten MCAPs bildet — der gemeinsame Strukturkern hinter sehr unterschiedlichen Anforderungswelten. Tieferer Wertnachweis als „eine weitere Norm unterstützt". Reine Aggregation, 0 Runtime, 0 neue Architektur.' % (len(core_cut), len(idx), len(cross))) +w("") + +print("\n".join(OUT)) diff --git a/backend-compliance/reference_scenarios/medical_stress_test.md b/backend-compliance/reference_scenarios/medical_stress_test.md new file mode 100644 index 00000000..231f5aff --- /dev/null +++ b/backend-compliance/reference_scenarios/medical_stress_test.md @@ -0,0 +1,26 @@ +# Medical Stress Test — Safety + Security gekoppelt, der härtere Test (Phase Ω #3) + +_Medical prüft erstmals gemeinsam: Safety UND Security gekoppelt, voller Produktlebenszyklus, sehr starke Risikomanagement-/Nachweispflichten, hohe regulatorische Tiefe. ISO 13485 als Company Profile durch DIESELBE Engine — nur neue Daten, 0 Runtime. Synthetisch, keine echten Namen._ + +## 1. ISO 13485 als Profil → Delta über dieselbe Engine +- ISO 13485 liefert medizinische QMS-Disziplin (Welt-1): `manage_document_control`, `operate_capa_process`, `conduct_design_controls`, `run_risk_management_process`. … +- Delta (fehlt): **11** Capabilities — über dieselbe `assess_transition`, **0 neue Runtime-Klassen**. + +## 2. Safety/Security-KOPPLUNG — Medical REUSED Cyber-MCAPs (IEC 81001-5-1 = CRA-Security) +- **Wiederverwendete Cyber-Capabilities (4):** `access_control_and_authentication`, `sbom_creation`, `secure_signed_update_distribution`, `technical_vulnerability_management`. +- → Genau das ist die Kopplung: die Gesundheitssoftware-Security (IEC 81001-5-1) fordert **dieselben** Fähigkeiten wie die CRA. Diese MCAPs wandern damit in eine **dritte Domäne** und werden im Convergence-Core noch zentraler. + +## 3. Genuin neue, medizin-spezifische Capabilities +- **Neu (7):** `assign_unique_device_identification`, `classify_software_safety_iec62304`, `compile_medical_technical_documentation`, `conduct_clinical_evaluation`, `implement_software_lifecycle_iec62304`, `maintain_risk_management_file_iso14971`, `perform_benefit_risk_analysis`. +- Capabilities als Verben: `conduct_clinical_evaluation`, `classify_software_safety_iec62304`, `maintain_risk_management_file_iso14971`, `perform_benefit_risk_analysis`. + +## 4. Was ISO 13485 typischerweise NICHT erzeugt _(rejected_assumptions, Welt-1/Welt-2)_ +- ISO 13485 does NOT produce clinical evidence or a clinical evaluation. +- ISO 13485 does NOT produce the ISO 14971 risk management FILE (only the process). +- ISO 13485 does NOT produce IEC 62304 software-lifecycle records or a software safety classification. +- ISO 13485 does NOT establish health-software security (IEC 81001-5-1 = the same security caps as the CRA). + +## Befund + +> **Die härteste Domäne bisher (Safety+Security gekoppelt, voller Lebenszyklus, tiefe Risiko-/Nachweispflicht) lief durch die unveränderte Pipeline — 0 neue Runtime-Klassen, nur ein Pattern-YAML.** Das stärkste Einzelsignal: **4 Security-Capabilities werden aus dem Cyber-Bestand WIEDERVERWENDET** (IEC 81001-5-1 = CRA), während **7 medizin-spezifische** neu hinzukommen. Genau so wächst ein Kern: gemeinsame Fähigkeiten verbinden Cyber, Maschinenbau, Automotive UND Medizin; das Domänenspezifische bleibt am Rand. Architecture Stability bleibt stabil; der Engpass ist jetzt die Qualität der Wissensmodellierung, nicht die Architektur. + diff --git a/backend-compliance/reference_scenarios/medical_stress_test.py b/backend-compliance/reference_scenarios/medical_stress_test.py new file mode 100644 index 00000000..09598109 --- /dev/null +++ b/backend-compliance/reference_scenarios/medical_stress_test.py @@ -0,0 +1,83 @@ +# ruff: noqa +# mypy: ignore-errors +"""Medical stress test — the harder scientific test: safety + security coupled (Phase Ω #3). + +Medical jointly tests properties not yet tested together: safety and security TIGHTLY COUPLED, a full +product lifecycle, very strong risk-management/evidence demands, high regulatory depth. ISO 13485 (a +medical QMS) is run as a Company Profile through the SAME engine as before — only new DATA, 0 runtime. + +The interesting result: IEC 81001-5-1 (health-software security) requires the SAME security capabilities +as the CRA, so Medical REUSES cyber MCAPs (the coupling shows up as capability reuse, growing the core), +while ALSO adding genuinely new medical caps (clinical evaluation, software safety classification, the +ISO 14971 risk file, benefit-risk). Synthetic, no real names. Non-runtime -> no deploy. +Run: cd backend-compliance && PYTHONPATH=. python3 reference_scenarios/medical_stress_test.py +""" +from __future__ import annotations + +import os +import yaml + +from compliance.company import ( + CompanyContext, Certification, CapabilityMappingEntry, build_company_profile, +) +from compliance.reasoning.enums import Confidence +from compliance.transition_reasoning import ( + TransitionContext, TransitionGoal, TargetRequirement, assess_transition, CoverageStatus, +) + +OUT = [] + + +def w(s=""): + OUT.append(s) + + +_TP = os.path.join(os.path.dirname(__file__), "..", "knowledge", "transition_patterns") +MED = yaml.safe_load(open(os.path.join(_TP, "transition_pattern_iso13485_to_medical_v1.yaml"), encoding="utf-8")) + +# existing capability universe (everything modelled before Medical) — to detect reuse vs new +known = set() +for f in os.listdir(_TP): + if f.endswith(".yaml") and "iso13485" not in f: + p = yaml.safe_load(open(os.path.join(_TP, f), encoding="utf-8")) + known |= {a["capability"] for a in p.get("likely_covered", [])} + known |= {d["capability"] for d in p.get("delta_requirements", [])} + +mgmt = [a["capability"] for a in MED["likely_covered"]] +delta_caps = [d["capability"] for d in MED["delta_requirements"]] +profile = build_company_profile( + CompanyContext(company_id="med", certifications=[Certification(certification_id="ISO13485")]), + {"ISO13485": CapabilityMappingEntry(capability_ids=mgmt, confidence=Confidence.MEDIUM)}) +assess = assess_transition(TransitionContext(company_id="med", target=TransitionGoal(target_id="Medical")), + [TargetRequirement(capability_id=c) for c in mgmt + delta_caps], profile) +delta = sorted({c.capability_id for c in assess.coverage if c.status == CoverageStatus.MISSING}) + +reused = sorted(c for c in delta if c in known) # safety/security coupling: cyber caps reused +fresh = sorted(c for c in delta if c not in known) # genuinely new medical caps + +w("# Medical Stress Test — Safety + Security gekoppelt, der härtere Test (Phase Ω #3)") +w("") +w('_Medical prüft erstmals gemeinsam: Safety UND Security gekoppelt, voller Produktlebenszyklus, sehr starke Risikomanagement-/Nachweispflichten, hohe regulatorische Tiefe. ISO 13485 als Company Profile durch DIESELBE Engine — nur neue Daten, 0 Runtime. Synthetisch, keine echten Namen._') +w("") +w("## 1. ISO 13485 als Profil → Delta über dieselbe Engine") +w("- ISO 13485 liefert medizinische QMS-Disziplin (Welt-1): %s." % ", ".join("`%s`" % c for c in mgmt[:4]) + " …") +w("- Delta (fehlt): **%d** Capabilities — über dieselbe `assess_transition`, **0 neue Runtime-Klassen**." % len(delta)) +w("") +w("## 2. Safety/Security-KOPPLUNG — Medical REUSED Cyber-MCAPs (IEC 81001-5-1 = CRA-Security)") +w("- **Wiederverwendete Cyber-Capabilities (%d):** %s." % (len(reused), ", ".join("`%s`" % c for c in reused))) +w("- → Genau das ist die Kopplung: die Gesundheitssoftware-Security (IEC 81001-5-1) fordert **dieselben** Fähigkeiten wie die CRA. Diese MCAPs wandern damit in eine **dritte Domäne** und werden im Convergence-Core noch zentraler.") +w("") +w("## 3. Genuin neue, medizin-spezifische Capabilities") +w("- **Neu (%d):** %s." % (len(fresh), ", ".join("`%s`" % c for c in fresh))) +w("- Capabilities als Verben: `conduct_clinical_evaluation`, `classify_software_safety_iec62304`, `maintain_risk_management_file_iso14971`, `perform_benefit_risk_analysis`.") +w("") +w("## 4. Was ISO 13485 typischerweise NICHT erzeugt _(rejected_assumptions, Welt-1/Welt-2)_") +for r in MED["rejected_assumptions"]: + w("- %s" % r) +w("") +w("## Befund") +w("") +w('> **Die härteste Domäne bisher (Safety+Security gekoppelt, voller Lebenszyklus, tiefe Risiko-/Nachweispflicht) lief durch die unveränderte Pipeline — 0 neue Runtime-Klassen, nur ein Pattern-YAML.** Das stärkste Einzelsignal: **%d Security-Capabilities werden aus dem Cyber-Bestand WIEDERVERWENDET** (IEC 81001-5-1 = CRA), während **%d medizin-spezifische** neu hinzukommen. Genau so wächst ein Kern: gemeinsame Fähigkeiten verbinden Cyber, Maschinenbau, Automotive UND Medizin; das Domänenspezifische bleibt am Rand. Architecture Stability bleibt stabil; der Engpass ist jetzt die Qualität der Wissensmodellierung, nicht die Architektur.' % (len(reused), len(fresh))) +w("") + +print("\n".join(OUT)) diff --git a/backend-compliance/reference_scenarios/mission_machine_builder.py b/backend-compliance/reference_scenarios/mission_machine_builder.py new file mode 100644 index 00000000..6b1e9fe8 --- /dev/null +++ b/backend-compliance/reference_scenarios/mission_machine_builder.py @@ -0,0 +1,171 @@ +# ruff: noqa +# mypy: ignore-errors +"""Customer Mission #1 — a full consulting simulation, NOT another architecture artifact. + +A Reference Scenario asks „is the knowledge correct?". A Customer Mission asks „can a customer actually +WORK with it?" — it forces the whole platform to behave as ONE connected expert system, from the first +question to a prioritised 6-month plan, and MEASURES how often the consultant had to „jump" (special-case +glue instead of a clean engine-to-engine handoff). If Mission #1 runs without jumps, the architecture is +probably done; the remaining work is knowledge, not foundation. + +Synthetic machine builder (NO real company names). Runs the REAL engines end-to-end. +Run: cd backend-compliance && PYTHONPATH=. python3 reference_scenarios/mission_machine_builder.py +Not product code; not imported by the app. Non-runtime -> no deploy. +""" +from __future__ import annotations + +import os +import yaml + +from compliance.profile.canonical import ( + CanonicalProductRegulatoryProfile as P, CanonicalProductType as PT, + EconomicOperatorRole as Role, CanonicalLifecyclePhase as LP, +) +from compliance.regulatory_map.renderer import render_regulatory_map +from compliance.company import CompanyContext, Certification, CapabilityMappingEntry, build_company_profile +from compliance.reasoning.enums import Confidence +from compliance.transition_reasoning import ( + TransitionContext, TransitionGoal, TargetRequirement, assess_transition, CoverageStatus, +) +from compliance.optimization import roadmap_from_delta, select_within_budget +from compliance.playbook import playbooks_for_plan +from compliance.completeness import assess_completeness + +OUT = [] +JUMPS = [] # (handoff, status, note) — the flow-continuity audit + + +def w(s=""): + OUT.append(s) + + +def step(handoff, status, note): + JUMPS.append((handoff, status, note)) + + +_HERE = os.path.dirname(__file__) +_K = os.path.join(_HERE, "..", "knowledge") + +w('# Customer Mission #1 — Maschinenbauer: „Was muss ich in den nächsten 6 Monaten tun?"') +w("") +w('_KEINE Demo, KEIN Reference Scenario — eine vollständige Simulation eines Beratungsprojekts mit den ECHTEN Engines. Gemessen wird, wie oft der Berater „springen" muss (Sonderlogik statt sauberem Engine-Fluss). Synthetischer Kunde, keine echten Namen._') +w("") +w("## Der Kunde (synthetisch)") +w("> ISO 9001 · ISMS (ISO 27001) · CE-Prozess · SPS · Fernwartung · Cloud · 80 Entwickler · Export EU") +w('> **Eine Frage:** „Was muss ich in den nächsten sechs Monaten tun?"') +w("") + +# ── 1. Scope — was gilt? (regulatory map) ───────────────────────────────── +prod = P(name="mb", product_type=PT.MACHINERY, markets=["EU"], economic_operator_role=Role.MANUFACTURER, + lifecycle_phase=LP.PLACING_ON_MARKET, is_machine=True, is_component=False, has_embedded_software=True, + connected_to_internet=True, has_remote_access=True, generates_usage_data=None) +rm = render_regulatory_map(prod) +appl = [v.regulation_id for v in rm.applicable_regulations] +unc = [v.regulation_id for v in rm.uncertain_regulations] +w("## 1. Scope — was gilt? _(Regulatory Map)_") +w("- **Gilt:** %s" % ", ".join(appl)) +w("- **Unsicher (Rückfrage):** %s" % ", ".join(unc)) +w("- **Overlaps:** %s" % ", ".join(ov.overlap_group_id for ov in rm.overlaps)) +w("") +step("Onboarding → Scope", "CLEAN", "Regulatory Map leitet aus dem Produktprofil CRA/MaschinenVO/EMV ab; RED/DataAct/NIS2 unsicher.") + +# ── 2. Journey — welche Übergänge? (certs + targets -> transitions) ──────── +# the company HAS ISO 27001 + ISO 9001; the product triggers CRA + MaschinenVO. +# THERE IS NO ENGINE that selects the journeys from (certs x targets) — we do it by hand here. +w("## 2. Journey — welche Übergänge? _(aus Zertifikaten + Zielen)_") +w("- Hat **ISO 27001 + ISO 9001**, Produkt = vernetzte Maschine → Ziel **CRA + MaschinenVO**.") +w("- Gewählte Journey: **ISO 27001 → CRA + MaschinenVO** (Convergence-Pattern) + QM-Seite ISO 9001 → MaschinenVO.") +w("- ⚠️ Die Übergänge stehen als DATEN in `knowledge/programs/transitions.yaml`, aber **keine Engine wählt sie aus Zertifikaten+Zielen** — hier manuell selektiert.") +w("") +step("Scope → Journey", "JUMP", "Kein Selektor-Engine `certs × applicable-targets → journeys` — die Journey-Wahl ist Glue (Daten existieren in transitions.yaml).") + +CP = yaml.safe_load(open(os.path.join(_K, "transition_patterns", "transition_pattern_iso27001_to_cra_maschinenvo_v1.yaml"), encoding="utf-8")) + +# ── 3. Capability Delta — was fehlt? (Company 2A + RS-005) ───────────────── +have = [a["capability"] for a in CP["likely_covered"]] +cmap = {"ISO27001": CapabilityMappingEntry(capability_ids=have, confidence=Confidence.MEDIUM)} +prof = build_company_profile(CompanyContext(company_id="mb", certifications=[Certification(certification_id="ISO27001")]), cmap) +reqs = [TargetRequirement(capability_id=a["capability"]) for a in CP["likely_covered"]] +reqs += [TargetRequirement(capability_id=d["capability"], question_intent=d.get("needed_information", "verify_existence")) + for d in CP["delta_requirements"]] +assess = assess_transition(TransitionContext(company_id="mb", target=TransitionGoal(target_id="CRA+MaschinenVO")), reqs, prof) +missing = sorted({c.capability_id for c in assess.coverage if c.status == CoverageStatus.MISSING}) +w("## 3. Capability Delta — was fehlt? _(Company 2A + RS-005)_") +w("> %s" % assess.summary.headline) +w("- Vermutlich vorhanden (aus ISMS, Welt 1): %s" % ", ".join(assess.summary.probably_covered[:4]) + " …") +w("- Fehlt (Delta): %d Capabilities, z. B. %s …" % (len(missing), ", ".join(missing[:4]))) +w("") +step("Journey → Capability Delta", "CLEAN", "assess_transition(Company-Profil, Required) → Coverage + Delta; sauberer Engine-Handoff.") +step("Zertifikate → Capabilities (Dependency)", "DEPENDENCY", "cert→capability-Map ist Execution-owned + injiziert (hier gemockt) — bewusste Ownership-Grenze, kein Architektur-Bruch.") + +# ── 4. Roadmap — was zuerst? (Optimization / Leverage) ──────────────────── +delta_t = {d["capability"]: d.get("covers_targets", []) for d in CP["delta_requirements"]} +opt = roadmap_from_delta(assess, delta_t) +bud = select_within_budget({m.capability_id: m.covers for m in opt.ranked_measures}, 5) +w("## 4. Roadmap — was zuerst? _(Optimization, größter Hebel)_") +w("> %s" % opt.headline) +w("- **Top-Maßnahmen:** %s" % ", ".join("`%s`(%d)" % (m.capability_id, m.leverage) for m in opt.ranked_measures[:5])) +w("") +step("Capability Delta → Roadmap", "CLEAN", "roadmap_from_delta(assessment, covers_targets) → Maßnahmen nach Hebel; sauber.") + +# ── 5. Playbooks — wie umsetzen? ────────────────────────────────────────── +kb = {} +for f in sorted(os.listdir(os.path.join(_K, "implementation_playbooks"))): + if f.endswith(".yaml"): + d = yaml.safe_load(open(os.path.join(_K, "implementation_playbooks", f), encoding="utf-8")) + kb[d["capability_id"]] = d +pbs = playbooks_for_plan(opt, kb) +have_pb = [p for p in pbs if p.status != "missing"] +w("## 5. Playbooks — wie umsetzen? _(Berater-Renderer)_") +w("- %d von %d Maßnahmen haben ein Playbook; %d brauchen Inhalt (Maschinensicherheits-Playbooks @IACE delegiert)." % (len(have_pb), len(pbs), len(pbs) - len(have_pb))) +w("") +step("Roadmap → Playbook", "CLEAN", "playbooks_for_plan(plan, knowledge) → Reise je Maßnahme; fehlender Inhalt = ehrliche `missing`-Stubs.") + +# ── 6. Nachweise — was belegen? ─────────────────────────────────────────── +ev = sorted({e for d in CP["delta_requirements"] for e in d.get("expected_evidence", [])}) +w("## 6. Nachweise — was belegen? _(expected_evidence)_") +w("- Geforderte Nachweise (Auszug): %s …" % ", ".join(ev[:6])) +w("") +step("Playbook → Evidence", "CLEAN", "expected_evidence trägt aus Pattern/Playbook durch — Datenfeld, kein Bruch.") + +# ── 7. Verification — kann ich es beweisen? ─────────────────────────────── +w("## 7. Verification — kann ich es BEWEISEN?") +w("- ⚠️ **Nicht gebaut** — der Verification-Layer (Evidence × Reality → bewiesen) ist Vision V2 (geparkt, Task #45).") +w("") +step("Evidence → Verification", "JUMP", "Verification-Layer fehlt (bewusst geparkt, Vision V2 / Requirements Verification Platform).") + +# ── 8. Completeness — wie sicher/vollständig? ───────────────────────────── +corpus = {r: ("validated" if r in ("CRA", "MaschinenVO", "DataAct") else "unsupported") for r in appl + unc} +rep = assess_completeness(appl + unc, corpus, + uncertain=[{"regulation": "DataAct", "deciding_question": "generates_usage_data", "reason": "generates_usage_data unbekannt"}]) +w("## 8. Completeness — wie sicher/vollständig? _(auditierbar)_") +w("> %s" % rep.completeness_summary) +w("- Offen/begründet: %s" % ", ".join("`%s`(%s)" % (e.subject, e.resolution) for e in rep.exclusions)) +w("") +step("Completeness (Dependency)", "DEPENDENCY", "corpus_status (welche Regelwerke validiert) wird kuratiert/injiziert, nicht aus dem Korpus abgeleitet.") + +# ── Die 6-Monats-Antwort ────────────────────────────────────────────────── +w("## Die 6-Monats-Antwort (Beratungsnarrativ)") +w("") +w('> „Sie sind als Maschinenbauer von **CRA + MaschinenVO** (und EMV) betroffen; RED/Data Act/NIS2 sind erst nach **einer Rückfrage** (`generates_usage_data`) zu klären. Ihr ISMS deckt die Informationssicherheits-Seite *wahrscheinlich* ab (zu bestätigen). Offen sind **%d Maßnahmen**. **Wenn Sie in den nächsten 6 Monaten die Top-5 nach regulatorischem Hebel umsetzen, schließen Sie %d von %d identifizierten Anforderungen (%d%%)** — beginnend mit den Maßnahmen, die CRA UND MaschinenVO gleichzeitig erfüllen. Für jede gibt es ein Umsetzungs-Playbook und die geforderten Nachweise; was wir noch NICHT bewerten konnten (EMV/RED/NIS2), weisen wir transparent aus."' % ( + len(missing), bud.requirements_closed, bud.total_requirements, int(round(bud.coverage_ratio * 100)))) +w("") + +# ── Flow-Continuity-Audit (der eigentliche Test) ────────────────────────── +clean = sum(1 for _, s, _ in JUMPS if s == "CLEAN") +jumps = sum(1 for _, s, _ in JUMPS if s == "JUMP") +deps = sum(1 for _, s, _ in JUMPS if s == "DEPENDENCY") +w("## Flow-Continuity-Audit — der eigentliche Test") +w("") +w("| Übergang | Status | Befund |") +w("|---|---|---|") +for h, s, n in JUMPS: + icon = {"CLEAN": "✅ sauber", "JUMP": "⚠️ SPRUNG", "DEPENDENCY": "🔌 Dependency"}[s] + w("| %s | %s | %s |" % (h, icon, n)) +w("") +w("**%d sauber · %d Sprünge · %d bewusste Dependencies.**" % (clean, jumps, deps)) +w("") +w("**Befund:** Die Plattform trägt den **gesamten Beratungsfluss** end-to-end — von der Kundenfrage bis zur priorisierten 6-Monats-Maßnahmenliste mit Playbooks, Nachweisen und ehrlicher Vollständigkeit. **Genau ZWEI echte Sprünge:** (1) **Scope → Journey** — es fehlt ein Selektor-Engine `Zertifikate × Ziele → Journeys` (die Daten existieren, nur die Auswahl ist Glue); (2) **Evidence → Verification** — bewusst geparkter Layer (Vision V2). Die zwei Dependencies (cert→capability-Map @Execution, corpus_status-Kuratierung) sind gewollte Ownership-Grenzen, keine Architektur-Brüche. → **Wenn der Scope→Journey-Selektor steht, ist das Fundament im Wesentlichen fertig; ab dann ist die Arbeit Wissen, nicht Architektur.**") +w("") + +print("\n".join(OUT)) diff --git a/backend-compliance/reference_scenarios/mission_multicert_cra.py b/backend-compliance/reference_scenarios/mission_multicert_cra.py new file mode 100644 index 00000000..1c33f425 --- /dev/null +++ b/backend-compliance/reference_scenarios/mission_multicert_cra.py @@ -0,0 +1,157 @@ +# ruff: noqa +# mypy: ignore-errors +"""Customer Mission #2 — the company arrives with a PROFILE, not a journey. + +Mission #1 (Maschinenbauer) ended on one open seam: „Scope → Journey" — a consultant had to hand-pick +„ISO 27001 → CRA". This mission deliberately STRESSES that seam with a multi-certified company (the +ETO-archetype: many certs at once) and asks the reframe question: is the start a *certification* you map +to a journey, or a *Company Capability Profile* you compute a delta from? + +Finding (proven below with the real engines): there is NO per-certificate journey. The start is the whole +profile; certifications are OBSERVATIONS that feed it; more evidence = smaller delta (12 → 9). The +„journey" is not selected — it is the delta `(Company Profile, Target)`. That shrinks Mission #1's jump: +the seam is not a journey-matcher, it is profile-intake + target-pick. + +Synthetic multi-certified company (NO real names). Runs the REAL engines end-to-end. +Run: cd backend-compliance && PYTHONPATH=. python3 reference_scenarios/mission_multicert_cra.py +Not product code; not imported by the app. Non-runtime -> no deploy. +""" +from __future__ import annotations + +import os +import yaml + +from compliance.company import ( + CompanyContext, Certification, CapabilityMappingEntry, build_company_profile, +) +from compliance.reasoning.enums import Confidence +from compliance.transition_reasoning import ( + TransitionContext, TransitionGoal, TargetRequirement, assess_transition, CoverageStatus, +) +from compliance.optimization import roadmap_from_delta, select_within_budget + +OUT = [] +RAT = [] # (question, answer) — the per-mission selection rationale the user asked to record + + +def w(s=""): + OUT.append(s) + + +def rationale(question, answer): + RAT.append((question, answer)) + + +_HERE = os.path.dirname(__file__) +_K = os.path.join(_HERE, "..", "knowledge") +CP = yaml.safe_load(open(os.path.join(_K, "transition_patterns", + "transition_pattern_iso27001_to_cra_maschinenvo_v1.yaml"), encoding="utf-8")) + +w('# Customer Mission #2 — „Wir haben SCHON viel. Was fehlt UNS noch für die CRA?"') +w("") +w('_Zweite Mission, bewusst ANDERS als #1: nicht „ein Zertifikat → ein Ziel", sondern ein hoch-zertifiziertes Unternehmen, das mit einem ganzen Profil ankommt. Test der einzigen offenen Naht aus Mission #1 (Scope → Journey). Synthetischer Kunde, keine echten Namen._') +w("") +w("## Der Kunde (synthetisch, hoch-zertifiziert)") +w("> **ISO 9001** · **ISO 27001** · **ISO 14001** · **TISAX** · **CE-Prozess** · **PSIRT** · vernetzte Maschinen · Export EU") +w('> **Eine Frage:** „Wir sind schon in vielem zertifiziert — was genau fehlt UNS noch, um CRA-konform zu sein?"') +w("") + +# ── 0. Company Capability Profile — DER Startzustand (nicht ein Zertifikat) ── +# Each certification is an OBSERVATION yielding probable capabilities. The profile is their AGGREGATE. +# Evidence is TARGET-RELATIVE: ISO 14001 is in the profile but irrelevant to the CRA (honest). +infosec = [a["capability"] for a in CP["likely_covered"]] +CERT_OBS = { + "ISO27001": (infosec, Confidence.MEDIUM, "Informationssicherheit (ISMS)"), + "TISAX": (infosec, Confidence.MEDIUM, "Automotive-ISMS — verstärkt dieselben Infosec-Fähigkeiten"), + "PSIRT": (["coordinated_vulnerability_disclosure", "exploited_vuln_and_incident_reporting"], + Confidence.HIGH, "Product-Security-Incident-Response — deckt ZWEI CRA-Delta-Fähigkeiten"), + "ISO9001": (["ce_conformity_assessment_and_technical_documentation"], + Confidence.MEDIUM, "QM-Dokumentendisziplin → CE-/Technische-Doku-Fähigkeit"), + "ISO14001": (["environmental_management_documentation"], + Confidence.MEDIUM, "Umweltmanagement — im Profil, aber für die CRA NICHT relevant"), +} +cmap = {k: CapabilityMappingEntry(capability_ids=v[0], confidence=v[1]) for k, v in CERT_OBS.items()} +ctx = CompanyContext(company_id="mc", certifications=[Certification(certification_id=k) for k in CERT_OBS]) +profile = build_company_profile(ctx, cmap) +w("## 0. Company Capability Profile — der eigentliche Startzustand") +w("> Das Unternehmen bringt **kein Zertifikat als Startpunkt**, sondern ein **Profil**. Jedes Zertifikat ist eine *Beobachtung*, die wahrscheinliche Fähigkeiten beisteuert; der Startzustand ist ihre **Aggregation**.") +w("") +w("| Zertifikat (Beobachtung) | steuert Fähigkeiten bei | Vertrauen |") +w("|---|---|---|") +for k, (caps, conf, note) in CERT_OBS.items(): + w("| **%s** — %s | %s | %s |" % (k, note, ", ".join("`%s`" % c for c in caps), conf.value)) +w("") +w("→ **Evidence ist zielrelativ:** ISO 14001 liegt im Profil, hilft der CRA aber **nicht**; PSIRT (oft übersehen) deckt **zwei** CRA-kritische Delta-Fähigkeiten. Genau deshalb darf man **nicht ein Zertifikat** zur Journey machen — das ganze Profil zählt.") +w("") + +# ── 1. Ziel (Intent) — was wollen SIE erreichen? ────────────────────────── +# The selection input is NOT a certificate. It is (Company Profile, Target). Intent = „CRA-konform werden". +TARGET = "CRA" +w("## 1. Ziel _(Intent)_ — was wollen Sie erreichen?") +w('- Intent: **„CRA-konform werden"** → Ziel-Profil = **CRA** (die von der CRA geforderten Fähigkeiten).') +w("- Auswahl-Eingabe ist damit **(Company Profile, Ziel)** — **kein** Zertifikat, das auf eine Journey gemappt wird.") +w("") + +# ── 2. Capability Delta — Profil → Ziel (das ist „die Journey") ──────────── +reqs = [TargetRequirement(capability_id=a["capability"]) for a in CP["likely_covered"]] +reqs += [TargetRequirement(capability_id=d["capability"], question_intent=d.get("needed_information", "verify_existence")) + for d in CP["delta_requirements"]] +assess = assess_transition(TransitionContext(company_id="mc", target=TransitionGoal(target_id=TARGET)), reqs, profile) +missing = sorted({c.capability_id for c in assess.coverage if c.status == CoverageStatus.MISSING}) + +# counterfactual: a single-certificate company (ISO 27001 only) — to show evidence shrinks the delta. +single_prof = build_company_profile( + CompanyContext(company_id="s", certifications=[Certification(certification_id="ISO27001")]), + {"ISO27001": CapabilityMappingEntry(capability_ids=infosec, confidence=Confidence.MEDIUM)}) +single_missing = sorted({c.capability_id for c in + assess_transition(TransitionContext(company_id="s", target=TransitionGoal(target_id=TARGET)), reqs, single_prof).coverage + if c.status == CoverageStatus.MISSING}) +closed_by_evidence = sorted(set(single_missing) - set(missing)) + +w('## 2. Capability Delta — Profil → CRA _(das IST die „Journey")_') +w("> %s" % assess.summary.headline) +w("- **Delta dieses Profils:** %d fehlende Fähigkeiten." % len(missing)) +w("- **Gegenprobe (nur ISO 27001):** %d fehlende Fähigkeiten." % len(single_missing)) +w("- **Mehr Evidence → kleineres Delta:** die zusätzliche Zertifizierung schließt **%d** Fähigkeiten *vorab*: %s." % ( + len(closed_by_evidence), ", ".join("`%s`" % c for c in closed_by_evidence))) +w("") +w("→ Es wurde **keine Journey ausgewählt**. Das Delta `(Profil, Ziel)` IST die Journey — berechnet, nicht gewählt. Die Journey ist nur die *Erklärung* dieses Deltas.") +w("") + +# ── 3. Roadmap — was zuerst? (gleicher Engine wie #1) ───────────────────── +delta_t = {d["capability"]: d.get("covers_targets", []) for d in CP["delta_requirements"]} +opt = roadmap_from_delta(assess, delta_t) +bud = select_within_budget({m.capability_id: m.covers for m in opt.ranked_measures}, 5) +w("## 3. Roadmap — was zuerst? _(gleicher Hebel-Engine wie Mission #1)_") +w("> %s" % opt.headline) +w("- Top-5 nach Hebel schließen **%d von %d** offenen Anforderungen (%d%%)." % ( + bud.requirements_closed, bud.total_requirements, int(round(bud.coverage_ratio * 100)))) +w("") + +# ── Selektions-Rationale (die vom User geforderten 5 Fragen) ─────────────── +rationale("Welche Journey wurde gewählt?", + "**Keine per-Zertifikat-Journey.** Die Journey ist `Company Capability Profile → CRA`. Gewählt wurde nur das **Ziel** (CRA); der Startzustand wurde aus ALLEN Zertifikaten aggregiert.") +rationale("Warum?", + 'Bei 6 Zertifikaten würde „ISO 27001 → CRA" die Evidence aus PSIRT/ISO 9001 wegwerfen (= 3 Fähigkeiten, die sonst fälschlich als Delta erschienen). Nur das **ganze Profil** ergibt das korrekte Delta.') +rationale("Welche Informationen waren für die Auswahl entscheidend?", + "(a) das **Ziel/Intent** (CRA) und (b) die **vollständige Zertifikatsliste** als Profil — **nicht** ein einzelnes Zertifikat. Welche Evidence hilft, ist **zielrelativ** (ISO 14001 irrelevant, PSIRT hoch-relevant).") +rationale("Musste das Journey-Modell erweitert werden?", + "**Konzeptionell ja, strukturell nein.** `from → to` bleibt; aber `from` ist ein **Company Capability Profile** (Multi-Cert-Aggregat), kein Zertifikat. Die Engines (`build_company_profile` + `assess_transition`) tun das BEREITS — es war ein Benenn-/Framing-Fehler, kein fehlender Code.") +rationale("Musste ein neuer Selektionsparameter eingeführt werden?", + "**Ja — und er VEREINFACHT.** Eingabe ist `(Company Profile, Ziel)`, nicht `(Zertifikat, Ziel)`. Die Zertifikate kollabieren ins Profil → **keine 2^N Cert-Kombinationen**, nur Profil→Ziel. Der Selektor wird damit kleiner, nicht größer.") +w("## Selektions-Rationale — die 5 Fragen (pro Mission zu dokumentieren)") +w("") +for q, a in RAT: + w("**%s** " % q) + w(a) + w("") + +# ── Was diese Mission über Mission #1 verändert ─────────────────────────── +w("## Was Mission #2 an Mission #1 verändert") +w("") +w('- Mission #1 nannte **Scope → Journey** einen Sprung („kein Selektor `certs × targets → journeys`"). Mission #2 zeigt: **diese Naht schrumpft.** Es gibt keinen Journey-Matcher zu bauen — die Journey ist das **berechnete Delta** `(Profil, Ziel)`. Was real fehlt, ist nur **Profil-Intake + Ziel-Wahl**, nicht eine Journey-Auswahl-Engine.') +w('- Bestätigt den Reframe: **Es gibt keine „ISO 27001 → CRA"-Transition — nur „Company Capability Profile → CRA".** Zertifikate sind **Beobachtungen/Evidence**, kein Journey-Startpunkt.') +w("- **Beobachtung für den (noch nicht gebauten) Selektor:** Eingabe = `(Company Profile, Ziel)`. Diversität über weitere Missionen muss zeigen, ob auch **Produktprofil** und **Intent-Klasse** als Parameter nötig werden — erst dann kanonisieren ([[rule-of-three-canonicalization]]).") +w("") + +print("\n".join(OUT)) diff --git a/backend-compliance/reference_scenarios/mission_non_security_target.py b/backend-compliance/reference_scenarios/mission_non_security_target.py new file mode 100644 index 00000000..5c3248ee --- /dev/null +++ b/backend-compliance/reference_scenarios/mission_non_security_target.py @@ -0,0 +1,129 @@ +# ruff: noqa +# mypy: ignore-errors +"""Customer Mission #5 — a NON-security target: does evidence relevance really flip? + +The whole „Evidence-Relevance(Target)" claim is only convincing if it holds in BOTH directions. Missions +#2–#4 showed security evidence (ISO 27001, PSIRT) ranking differently across security targets, and ISO +14001 being worthless against the CRA/TISAX. This mission closes the loop with a deliberately NON-security +target — an environmental / material-evidence requirement set — and asks the one question: + + is `relevance(evidence, target)` genuinely a function of the TARGET, or an attribute of the evidence? + +Expected and shown: ISO 14001 is keine/niedrig against CRA and TISAX but HOCH against the environmental +target — while the security certificates (ISO 27001, PSIRT) flip the other way (relevant for security, +keine for the environmental target). The same evidence is worthless or decisive depending on the target. + +DELIBERATELY NOT here (per scope): no environmental corpus, no ISO-14001 norm model, no new runtime +module, no deploy, no real names. The environmental target is a hand-authored Required set (injected like +any TargetRequirement) purely to point the existing engine at a non-security goal. + +Run: cd backend-compliance && PYTHONPATH=. python3 reference_scenarios/mission_non_security_target.py +""" +from __future__ import annotations + +import os +import yaml + +from compliance.company import ( + CompanyContext, Certification, CapabilityMappingEntry, build_company_profile, +) +from compliance.reasoning.enums import Confidence +from compliance.transition_reasoning import ( + TransitionContext, TransitionGoal, TargetRequirement, assess_transition, CoverageStatus, +) + +OUT = [] + + +def w(s=""): + OUT.append(s) + + +_K = os.path.join(os.path.dirname(__file__), "..", "knowledge", "transition_patterns") + + +def _caps(pattern_file): + p = yaml.safe_load(open(os.path.join(_K, pattern_file), encoding="utf-8")) + return [a["capability"] for a in p["likely_covered"]] + [d["capability"] for d in p["delta_requirements"]] + + +# ── Three targets — two security, one NON-security (the new axis) ─────────────────────────── +CRA = _caps("transition_pattern_iso27001_to_cra_maschinenvo_v1.yaml") # security regulation +TISAX = _caps("transition_pattern_isms_to_tisax_v1.yaml") # security certification +# NON-security target: an environmental / material-evidence requirement set. Hand-authored Required +# capabilities (NOT a corpus, NOT an ISO-14001 norm model) — just a goal to point the engine at. +ENVIRONMENTAL = [ + "environmental_management_documentation", "energy_efficiency_documentation", + "supply_chain_environmental_due_diligence", "material_declaration_scip_reach", + "hazardous_substance_restriction_rohs", "carbon_footprint_accounting", + "recycling_and_take_back", "battery_passport_material_data", +] +TARGETS = [("CRA", "Security/Regulation", CRA), ("TISAX", "Security/Certification", TISAX), + ("Umwelt-/Materialnachweis", "NICHT-Security", ENVIRONMENTAL)] + +# ── ONE company profile (the same multi-certified archetype; it HAS ISO 14001) ────────────── +CERT_OBS = { + "ISO27001": ["information_security_management", "incident_management", "access_control_and_authentication", + "technical_vulnerability_management", "security_logging_and_monitoring", "secure_development_lifecycle"], + "TISAX": ["information_security_management", "access_control_and_authentication", "incident_management", + "supplier_security", "physical_security", "prototype_protection"], + "PSIRT": ["coordinated_vulnerability_disclosure", "exploited_vuln_and_incident_reporting", + "public_security_advisories"], + # an EMS (ISO 14001) touches several environmental process areas — relevant ONLY to an env target: + "ISO14001": ["environmental_management_documentation", "energy_efficiency_documentation", + "supply_chain_environmental_due_diligence"], + "ISO9001": ["ce_conformity_assessment_and_technical_documentation"], +} +cmap = {k: CapabilityMappingEntry(capability_ids=v, confidence=Confidence.MEDIUM) for k, v in CERT_OBS.items()} +profile = build_company_profile( + CompanyContext(company_id="mc5", certifications=[Certification(certification_id=k) for k in CERT_OBS]), cmap) + + +def _delta(caps): + reqs = [TargetRequirement(capability_id=c) for c in caps] + a = assess_transition(TransitionContext(company_id="mc5", target=TransitionGoal(target_id="t")), reqs, profile) + return sorted({c.capability_id for c in a.coverage if c.status == CoverageStatus.MISSING}) + + +def _rel(cert_caps, target_caps): + n = len(set(cert_caps) & set(target_caps)) + return n, ("hoch" if n >= 3 else "niedrig" if n >= 1 else "keine") + + +w('# Customer Mission #5 — ein Nicht-Security-Ziel: kippt die Evidence-Relevanz?') +w("") +w('_Enger Scope: KEIN Umweltrecht, KEINE ISO-14001-Normmodellierung, KEIN neues Modul, KEIN Deploy. Nur die EINE Frage: ist `relevance(evidence, target)` wirklich eine Funktion des Ziels — oder ein Attribut der Evidence? Das Umwelt-/Materialnachweis-Ziel ist ein hand-authored `Required`-Satz (synthetisch), nur um die bestehende Engine auf ein Nicht-Security-Ziel zu richten. Synthetischer Kunde, keine echten Namen._') +w("") +w("## Der Kunde (synthetisch) — EIN Profil (hat u. a. ISO 14001)") +w("> **ISO 9001 · ISO 27001 · ISO 14001 · TISAX · PSIRT** · vernetzte Maschinen · Export EU") +w("") + +# ── 1. Die Relevanz-Matrix (zwei Security-Ziele + ein Umwelt-Ziel) ───────── +w("## 1. Evidence-Relevanz über drei Ziele — zwei Security, eines NICHT") +w("| Zertifizierung (Evidence) | → CRA | → TISAX | → Umwelt-/Material |") +w("|---|---|---|---|") +for cert, caps in CERT_OBS.items(): + cells = [_rel(caps, t)[1] + " (%d)" % _rel(caps, t)[0] for _, _, t in TARGETS] + mark = " ⟵ kippt" if cert == "ISO14001" else "" + w("| **%s** | %s | %s | %s |%s" % (cert, cells[0], cells[1], cells[2], mark)) +w("") + +# ── 2. Der Beweis: dieselbe Evidence, gegensätzlicher Wert je Ziel ───────── +iso14_env = _rel(CERT_OBS["ISO14001"], ENVIRONMENTAL) +iso27_env = _rel(CERT_OBS["ISO27001"], ENVIRONMENTAL) +psirt_env = _rel(CERT_OBS["PSIRT"], ENVIRONMENTAL) +w("## 2. Beweis — Relevanz ist eine Funktion des ZIELS (in BEIDE Richtungen)") +w("- **ISO 14001:** gegen CRA/TISAX **keine**, gegen das Umwelt-/Materialziel **%s (%d)**. Dieselbe Zertifizierung — von wertlos zu entscheidend, nur weil das Ziel wechselt." % (iso14_env[1], iso14_env[0])) +w("- **Symmetrisch:** **ISO 27001** (hoch gegen CRA/TISAX) ist gegen das Umwelt-/Materialziel **%s (%d)**; **PSIRT** ebenso **%s (%d)**. Security-Evidence ist hier wertlos." % (iso27_env[1], iso27_env[0], psirt_env[1], psirt_env[0])) +w("- Delta des Umwelt-/Materialziels: **%d** fehlende Fähigkeiten (das Profil deckt nur die ISO-14001-nahen ab) — über dieselbe `assess_transition`-Engine, kein Sonderpfad." % len(_delta(ENVIRONMENTAL))) +w("") +w('→ **Damit ist `relevance(evidence, target)` zweiseitig bewiesen:** keine Evidence ist „an sich" relevant; Relevanz entsteht erst gegen ein Ziel. Eine Capability/Zertifizierung ohne Ziel hat keinen Relevanzwert.') +w("") + +# ── Befund ──────────────────────────────────────────────────────────────── +w("## Befund") +w("") +w('> **Ein und dieselbe Evidence kann je Ziel wertlos oder hoch relevant sein** — hier erstmals an einem NICHT-Security-Ziel gezeigt, in beide Richtungen (ISO 14001 kippt von keine→hoch, Security-Certs von hoch→keine). Folgerung für das spätere Modell: Relevanz darf NICHT als Attribut der Evidence gespeichert werden, sondern nur als `relevance(evidence, target)` berechnet (computed-not-stored). **Damit ist die Ziel-Diversität für den späteren Selektor beisammen: Regulation · Certification · Contract/Tender · OEM-Spec · Umwelt-/Material-Ziel — fünf Zielarten durch dieselbe Engine. Erst jetzt wird ein Scope→Journey-Selektor sinnvoll** (er optimiert nicht mehr auf einer einzigen Zielart).') +w("") + +print("\n".join(OUT)) diff --git a/backend-compliance/reference_scenarios/mission_second_contract.py b/backend-compliance/reference_scenarios/mission_second_contract.py new file mode 100644 index 00000000..8dc7d1a5 --- /dev/null +++ b/backend-compliance/reference_scenarios/mission_second_contract.py @@ -0,0 +1,134 @@ +# ruff: noqa +# mypy: ignore-errors +"""Customer Mission #4 — a SECOND, different contract target (no tender-special-logic). + +Mission #3 showed one contract (a public tender) runs through the same engine as a regulation and a +certification. One contract is not enough: with a single example we might accidentally bake +tender-shaped assumptions into the later Scope→Journey selector. So this mission runs TWO deliberately +different contract sub-types against the same company through the same engine: + + - a PUBLIC TENDER (öffentliche Ausschreibung — procurement: pentest report, references, SLA) + - a PRIVATE OEM SPEC (Lastenheft — automotive customer: CSMS, functional safety, SUMS, ASPICE) + +If both reduce to a plain `Required` set and produce DIFFERENT deltas with the IDENTICAL engine and NO +per-contract code, then „Contract" is not a special case — it is just another requirement source, and +the selector can treat any contract uniformly. + +Synthetic company + synthetic contracts (NO real names). Runs the REAL engine. Non-runtime -> no deploy. +Run: cd backend-compliance && PYTHONPATH=. python3 reference_scenarios/mission_second_contract.py +""" +from __future__ import annotations + +from compliance.company import ( + CompanyContext, Certification, CapabilityMappingEntry, build_company_profile, +) +from compliance.reasoning.enums import Confidence +from compliance.transition_reasoning import ( + TransitionContext, TransitionGoal, TargetRequirement, assess_transition, CoverageStatus, +) + +OUT = [] + + +def w(s=""): + OUT.append(s) + + +# ── Two contracts, each just a set of required capabilities (a contract has no parser — injected) ── +TENDER = [ # public procurement: security baseline + procurement-specific evidence + "information_security_management", "access_control_and_authentication", "incident_management", + "technical_vulnerability_management", "coordinated_vulnerability_disclosure", "sbom_creation", + "supplier_security", "penetration_test_evidence", "reference_project_evidence", + "security_sla_and_support_commitment", +] +OEM_SPEC = [ # private automotive customer Lastenheft: security + automotive-engineering-specific + "information_security_management", "access_control_and_authentication", "incident_management", + "supplier_security", "prototype_protection", "secure_signed_update_distribution", + "dedicated_security_contact", "cybersecurity_management_system", "software_update_management_system", + "functional_safety_evidence", "aspice_process_capability", +] +CONTRACTS = [ + ("Öffentliche Ausschreibung", "public tender", TENDER), + ("OEM-Lastenheft", "private OEM spec", OEM_SPEC), +] + +# ── ONE company profile (same multi-certified archetype as Missions #2/#3) ────────────────── +CERT_OBS = { + "ISO27001": ["information_security_management", "incident_management", "access_control_and_authentication", + "technical_vulnerability_management", "security_logging_and_monitoring", + "secure_development_lifecycle", "cybersecurity_management_system"], + "TISAX": ["information_security_management", "access_control_and_authentication", "incident_management", + "supplier_security", "prototype_protection", "cybersecurity_management_system"], + "PSIRT": ["coordinated_vulnerability_disclosure", "dedicated_security_contact", + "secure_signed_update_distribution"], + "ISO9001": ["ce_conformity_assessment_and_technical_documentation"], + "ISO14001": ["environmental_management_documentation"], + "CE": ["ce_conformity_assessment_and_technical_documentation"], +} +cmap = {k: CapabilityMappingEntry(capability_ids=v, confidence=Confidence.MEDIUM) for k, v in CERT_OBS.items()} +profile = build_company_profile( + CompanyContext(company_id="mc4", certifications=[Certification(certification_id=k) for k in CERT_OBS]), cmap) + + +def _delta(req_caps): + reqs = [TargetRequirement(capability_id=c) for c in req_caps] + a = assess_transition(TransitionContext(company_id="mc4", target=TransitionGoal(target_id="c")), reqs, profile) + return sorted({c.capability_id for c in a.coverage if c.status == CoverageStatus.MISSING}) + + +def _relevance(cert_caps, req_caps): + n = len(set(cert_caps) & set(req_caps)) + return n, ("hoch" if n >= 3 else "mittel" if n >= 1 else "keine") + + +w('# Customer Mission #4 — zwei verschiedene Verträge, eine Engine (kein Contract-Spezialfall)') +w("") +w('_Mission #3 zeigte EINEN Vertrag (öffentliche Ausschreibung) durch dieselbe Engine wie Gesetz/Zertifizierung. Ein Beispiel reicht nicht — sonst backen wir Tender-Annahmen in den späteren Selektor. Hier laufen ZWEI bewusst unterschiedliche Vertragsarten gegen dasselbe Unternehmen. Synthetischer Kunde + synthetische Verträge, keine echten Namen._') +w("") +w("## Der Kunde (synthetisch) — EIN Profil") +w("> **ISO 9001 · ISO 27001 · ISO 14001 · TISAX · CE-Prozess · PSIRT** · vernetzte Maschinen · Export EU") +w("") + +# ── 1. Zwei Verträge durch DIESELBE Engine ──────────────────────────────── +deltas = {name: _delta(caps) for name, _, caps in CONTRACTS} +w("## 1. Zwei Vertragsarten — dieselbe Engine `Profil − Required = Delta`") +w("") +w("| Vertrag | Art | geforderte Fähigkeiten | Delta (fehlt) |") +w("|---|---|---|---|") +for name, kind, caps in CONTRACTS: + w("| **%s** | %s | %d | **%d** |" % (name, kind, len(caps), len(deltas[name]))) +w("") +w("→ Beide Verträge sind nur ein `Required`-Satz; **es gibt keinen Contract-spezifischen Codepfad** — `assess_transition` behandelt sie identisch zu Gesetz und Zertifizierung.") +w("") + +# ── 2. Die Deltas sind UNTERSCHIEDLICH (also echte verschiedene Verträge) ── +only_t = sorted(set(deltas["Öffentliche Ausschreibung"]) - set(deltas["OEM-Lastenheft"])) +only_o = sorted(set(deltas["OEM-Lastenheft"]) - set(deltas["Öffentliche Ausschreibung"])) +shared = sorted(set(deltas["Öffentliche Ausschreibung"]) & set(deltas["OEM-Lastenheft"])) +w("## 2. Verschiedene Verträge → verschiedene Deltas (Beweis: keine Tender-Speziallogik nötig)") +w("- **Nur Ausschreibung:** %s" % (", ".join("`%s`" % c for c in only_t) or "—")) +w("- **Nur OEM-Lastenheft:** %s" % (", ".join("`%s`" % c for c in only_o) or "—")) +w("- **Beiden gemeinsam:** %s" % (", ".join("`%s`" % c for c in shared) or "—")) +w("") +w("→ Die zwei Verträge fordern **strukturell anderes** (Beschaffungsnachweise vs. Automotive-Engineering: CSMS/funktionale Sicherheit/SUMS/ASPICE). Trotzdem **ein** Mechanismus. Genau das brauchten wir vor dem Selektor: zwei diverse Contract-Ziele, kein Sonderpfad.") +w("") + +# ── 3. Evidence-Relevance(Vertrag) — dieselbe Evidence, anderer Wert je Vertrag ── +w("## 3. Evidence-Relevanz(Vertrag)") +w("| Zertifizierung (Evidence) | → Ausschreibung | → OEM-Lastenheft |") +w("|---|---|---|") +for cert, caps in CERT_OBS.items(): + nt, lt = _relevance(caps, TENDER) + no, lo = _relevance(caps, OEM_SPEC) + w("| **%s** | %s (%d) | %s (%d) |" % (cert, lt, nt, lo, no)) +w("") +w("→ **TISAX** zählt gegen den **Automotive-OEM** mehr als gegen die generische Ausschreibung (Prototype Protection, CSMS); **ISO 14001** ist gegen beide **keine**. Bestätigt: **Relevanz ist eine Funktion des Ziels** — auch zwischen zwei Verträgen.") +w("") + +# ── Befund ──────────────────────────────────────────────────────────────── +w("## Befund") +w("") +w('> **Zwei strukturell verschiedene Verträge, ein Mechanismus, keine Zeile Contract-Spezialcode.** Damit ist „Contract" als Anforderungsquelle abgesichert (≥2 diverse Fälle): der spätere Scope→Journey-Selektor kann **jeden** Vertrag als reinen `Required`-Satz behandeln, ohne Tender-Speziallogik. Nächste sinnvolle Diversität vor dem Selektor: ein Vertrag, der bewusst auf NICHT-Security-Fähigkeiten zielt (z. B. Umwelt-/Materialnachweise), um auch die zielrelative Evidence-Relevanz über Domänen hinweg zu prüfen.') +w("") + +print("\n".join(OUT)) diff --git a/backend-compliance/reference_scenarios/mission_three_target_types.py b/backend-compliance/reference_scenarios/mission_three_target_types.py new file mode 100644 index 00000000..01d21712 --- /dev/null +++ b/backend-compliance/reference_scenarios/mission_three_target_types.py @@ -0,0 +1,159 @@ +# ruff: noqa +# mypy: ignore-errors +"""Customer Mission #3 — ONE company profile, THREE target TYPES (the Requirements Verification proof). + +Mission #2 proved the start is a Company Capability Profile, not a certificate. This mission proves the +NEXT thing: the same pipeline does not care what KIND of target it is pointed at. We run ONE profile +against three deliberately different target types — + + 1. a REGULATION (CRA) — law + 2. a CERTIFICATION (TISAX) — scheme + 3. a CONTRACT (public tender) — buyer requirement (synthetic, hand-authored) + +— through the identical engine (assess_transition). If all three run, the architecture is not a +„compliance" system but a Requirements Verification platform: a regulation, a certification and a +contract all reduce to required capabilities, and `Profile − Required = Delta` is target-type-agnostic. + +It also makes the user's „Evidence-Relevance(Target)" concrete: the SAME evidence (a certification) +is worth a different amount against a different target — ISO 14001 is irrelevant to all three security +targets here, yet would be decisive against an environmental target. + +Synthetic company + synthetic tender (NO real names). Runs the REAL engines. Non-runtime -> no deploy. +Run: cd backend-compliance && PYTHONPATH=. python3 reference_scenarios/mission_three_target_types.py +""" +from __future__ import annotations + +import os +import yaml + +from compliance.company import ( + CompanyContext, Certification, CapabilityMappingEntry, build_company_profile, +) +from compliance.reasoning.enums import Confidence +from compliance.transition_reasoning import ( + TransitionContext, TransitionGoal, TargetRequirement, assess_transition, CoverageStatus, +) + +OUT = [] + + +def w(s=""): + OUT.append(s) + + +_K = os.path.join(os.path.dirname(__file__), "..", "knowledge", "transition_patterns") + + +def _caps(pattern_file): + p = yaml.safe_load(open(os.path.join(_K, pattern_file), encoding="utf-8")) + return [a["capability"] for a in p["likely_covered"]] + [d["capability"] for d in p["delta_requirements"]] + + +# ── The three targets, each a different TYPE, each just a set of required capabilities ────── +CRA = _caps("transition_pattern_iso27001_to_cra_maschinenvo_v1.yaml") # REGULATION +TISAX = _caps("transition_pattern_isms_to_tisax_v1.yaml") # CERTIFICATION +# CONTRACT: a synthetic public tender for a networked industrial machine. Hand-authored required +# capabilities (a tender has no parser yet — injected exactly like Execution-placeholder requirements). +TENDER = [ + "information_security_management", "access_control_and_authentication", "incident_management", + "technical_vulnerability_management", "coordinated_vulnerability_disclosure", "sbom_creation", + "supplier_security", "data_protection_processing_on_behalf", + "penetration_test_evidence", "reference_project_evidence", "security_sla_and_support_commitment", +] +TARGETS = [ + ("CRA", "Regulation", "regulatorisch", CRA), + ("TISAX", "Certification", "Zertifizierung", TISAX), + ("Öffentliche Ausschreibung", "Contract", "vertraglich (synthetisch)", TENDER), +] + +# ── ONE Company Capability Profile (the same multi-certified company as Mission #2) ───────── +# Each certification is an OBSERVATION contributing probable capabilities; ISO 14001 is in the +# profile but contributes nothing any of the three security/quality targets needs (target-relative). +CERT_OBS = { + "ISO27001": ["information_security_management", "incident_management", "access_control_and_authentication", + "technical_vulnerability_management", "security_logging_and_monitoring", + "secure_development_lifecycle", "asset_and_configuration_management", "cryptography"], + "TISAX": ["information_security_management", "access_control_and_authentication", "incident_management", + "supplier_security", "physical_security", "security_awareness_training", + "prototype_protection", "data_protection_processing_on_behalf"], + "PSIRT": ["coordinated_vulnerability_disclosure", "exploited_vuln_and_incident_reporting", + "public_security_advisories"], + "ISO9001": ["ce_conformity_assessment_and_technical_documentation"], + "ISO14001": ["environmental_management_documentation"], + "CE": ["ce_conformity_assessment_and_technical_documentation"], +} +cmap = {k: CapabilityMappingEntry(capability_ids=v, confidence=Confidence.MEDIUM) for k, v in CERT_OBS.items()} +profile = build_company_profile( + CompanyContext(company_id="mc3", certifications=[Certification(certification_id=k) for k in CERT_OBS]), cmap) + + +def _delta(target_caps): + reqs = [TargetRequirement(capability_id=c) for c in target_caps] + a = assess_transition(TransitionContext(company_id="mc3", target=TransitionGoal(target_id="t")), reqs, profile) + missing = sorted({c.capability_id for c in a.coverage if c.status == CoverageStatus.MISSING}) + return len(target_caps), missing + + +def _relevance(cert_caps, target_caps): + n = len(set(cert_caps) & set(target_caps)) + return n, ("hoch" if n >= 3 else "mittel" if n >= 1 else "keine") + + +w('# Customer Mission #3 — EIN Profil, DREI Zieltypen (der Requirements-Verification-Beweis)') +w("") +w('_Mission #2 bewies: der Start ist ein Company Capability Profile, kein Zertifikat. Mission #3 beweist das Nächste: dieselbe Pipeline ist ZIELTYP-AGNOSTISCH. Ein Gesetz, eine Zertifizierung und ein Vertrag reduzieren sich alle auf geforderte Fähigkeiten. Synthetischer Kunde + synthetische Ausschreibung, keine echten Namen._') +w("") +w("## Der Kunde (synthetisch) — EIN Profil") +w("> **ISO 9001 · ISO 27001 · ISO 14001 · TISAX · CE-Prozess · PSIRT** · vernetzte Maschinen · Export EU") +w("") + +# ── 1. Drei Zieltypen durch DIESELBE Engine ─────────────────────────────── +w("## 1. Drei Zieltypen — dieselbe Engine `Profil − Required = Delta`") +w("") +w("| Ziel | Zieltyp | geforderte Fähigkeiten | Delta (fehlt) |") +w("|---|---|---|---|") +deltas = {} +for name, kind, _, caps in TARGETS: + total, missing = _delta(caps) + deltas[name] = (total, missing) + w("| **%s** | %s | %d | **%d** |" % (name, kind, total, len(missing))) +w("") +w("→ Drei **völlig unterschiedliche Zielarten** (Gesetz · Zertifizierung · Vertrag) liefen durch **eine** Engine ohne Sonderfall. Der Vertrag (Ausschreibung) ist nur ein weiterer `Required`-Satz — genau das ist die **Requirements Verification Platform** ([[strategy-requirements-intelligence]]): die Anforderungs-QUELLE ist austauschbar, die Pipeline bleibt.") +w("") + +# ── 2. Cross-Target-Typ-Konvergenz ──────────────────────────────────────── +all_caps = {} +for name, _, _, caps in TARGETS: + for c in caps: + all_caps.setdefault(c, []).append(name) +multi = {c: ts for c, ts in all_caps.items() if len(ts) >= 2} +w("## 2. Konvergenz über Zieltypen hinweg") +w("- **%d Fähigkeiten erfüllen ≥2 der drei Zielarten gleichzeitig** — eine Maßnahme zahlt auf Gesetz UND Zertifizierung UND/ODER Vertrag ein." % len(multi)) +w("- Beispiele (Fähigkeit → Ziele): %s" % "; ".join( + "`%s`→{%s}" % (c, ",".join(ts)) for c, ts in list(multi.items())[:4])) +w("- Das ist der Hebel *eine Ebene höher* als Mission #1: dort konvergierten **Gesetze** (CRA+MaschinenVO), hier konvergieren **ZielTYPEN**.") +w("") + +# ── 3. Evidence-Relevance(Target) — dieselbe Evidence, anderer Wert je Ziel ─ +w("## 3. Evidence-Relevanz(Ziel) — dieselbe Zertifizierung, anderer Wert je Ziel") +w('> Nicht „Evidence vorhanden", sondern **Evidence-Relevanz(Ziel)**: jede Zertifizierung wird **relativ zum Ziel** bewertet. Das erklärt, warum dieselbe Capability in zwei Beratungen unterschiedlich priorisiert wird.') +w("") +w("| Zertifizierung (Evidence) | → CRA | → TISAX | → Ausschreibung |") +w("|---|---|---|---|") +for cert, caps in CERT_OBS.items(): + cells = [] + for _, _, _, tcaps in TARGETS: + n, lvl = _relevance(caps, tcaps) + cells.append("%s (%d)" % (lvl, n)) + w("| **%s** | %s | %s | %s |" % (cert, cells[0], cells[1], cells[2])) +w("") +w("→ **PSIRT** ist gegen die **CRA hoch**, gegen **TISAX keine**, gegen die **Ausschreibung mittel** — dieselbe Evidence, drei verschiedene Werte. **ISO 14001** ist gegen alle drei (Security/Qualität) **keine** — *aber* gegen ein **Umwelt-Ziel** (Batterieverordnung/Umweltauflagen) wäre sie **hoch**. Genau deshalb gilt: **Relevanz ist eine Funktion des Ziels, kein Attribut der Evidence.**") +w("") + +# ── Befund ──────────────────────────────────────────────────────────────── +w("## Befund") +w("") +w('> **Dieselbe Pipeline trägt Gesetz, Zertifizierung UND Vertrag.** Damit ist bewiesen, dass die Architektur nicht „Compliance" macht, sondern **Anforderungen verifiziert** — die Quelle (Regulation/Certification/Contract) ist nur ein `Required`-Satz, der Rest ist `Profil − Required = Delta`. Zwei durable Folgerungen: (1) **Evidence-Relevanz ist zielrelativ** (gehört künftig als `relevance(evidence, target)` modelliert, nicht als „vorhanden/fehlt"); (2) Konvergenz existiert nicht nur zwischen Gesetzen, sondern zwischen **Zielarten** — der höchste Hebel überhaupt.') +w("") + +print("\n".join(OUT)) diff --git a/backend-compliance/reference_scenarios/onboarding_advisor_demo.md b/backend-compliance/reference_scenarios/onboarding_advisor_demo.md new file mode 100644 index 00000000..db964dd5 --- /dev/null +++ b/backend-compliance/reference_scenarios/onboarding_advisor_demo.md @@ -0,0 +1,51 @@ +# Smart Onboarding Advisor — was der Nutzer sieht (automatisch, ohne Vertrieb) + +_Eingabe: Unternehmen + Produkte + Zertifizierungen + Ziel. Den Rest macht die Orchestrierung über die bestehenden Engines (Company 2A · RS-005 · Optimization · Completeness). Synthetisch, keine echten Namen._ + +## Eingabe +> Zertifizierungen: **ISO9001, ISO27001, ISO14001, TISAX** · Produkt: **Parkschein-/Schrankensystem** · Ziel: **CRA** + +## Phase 0 — Stille Vorbefüllung (BEVOR eine Frage erscheint) +- **Signal Producer (verschiedene Dialekte → ein kanonisches Signal):** `vdp_found`(website), `cyclonedx_found`(repository), `cosign_found`(repository), `risk_assessment_pdf`(document), `cloud_hosted`(product), `plc_detected`(product) +> Stille Vorbefüllung: 4 Fähigkeit(en) automatisch erkannt, 2 Produktfakt(en), 4 Nachweis(e) bereits vorhanden. +- **Automatisch erkannte Fähigkeiten:** `coordinated_vulnerability_disclosure`, `product_cyber_risk_assessment`, `sbom_creation`, `secure_signed_update_distribution` +- **Produktfakten (steuern den Scope):** `connected_to_internet=true`, `is_machine=true` +- **Nachweise bereits in der Hand (kein Upload nötig):** cvd_policy, product_risk_assessment, sbom, signing_config + +## Was wir erkannt haben +> 17 Anforderungen erkannt · 4 automatisch erkannt (Intake) · 5 wahrscheinlich (Zertifikate) · 5 zu klären + +**Aus Ihren Zertifizierungen abgeleitet (zu bestätigen, nicht automatisch erfüllt):** +- ISO9001 legt 1 relevante Fähigkeit(en) nahe — Verifikation erforderlich, nicht automatisch erfüllt +- ISO27001 legt 4 relevante Fähigkeit(en) nahe — Verifikation erforderlich, nicht automatisch erfüllt +- TISAX legt 4 relevante Fähigkeit(en) nahe — Verifikation erforderlich, nicht automatisch erfüllt +- _ISO14001 ist für dieses Ziel nicht relevant — relevance(evidence, target) = 0 — keine geforderte Fähigkeit abgedeckt_ + +## Die wenigen offenen Punkte — nur die nächsten besten Fragen +**Frage 1 von 5** _(Informationswert 8)_ +> protection against corruption of safety functions? — _Warum fragen wir das: Keine Anhaltspunkte im Unternehmensprofil — klären._ + +**Frage 2 von 5** _(Informationswert 7)_ +> exploited vuln and incident reporting? — _Warum fragen wir das: Keine Anhaltspunkte im Unternehmensprofil — klären._ + +**Frage 3 von 5** _(Informationswert 7)_ +> machine safety risk assessment? — _Warum fragen wir das: Keine Anhaltspunkte im Unternehmensprofil — klären._ + +**Frage 4 von 5** _(Informationswert 7)_ +> mechanical safety and guards? — _Warum fragen wir das: Keine Anhaltspunkte im Unternehmensprofil — klären._ + +**Frage 5 von 5** _(Informationswert 7)_ +> operating instructions and safety information? — _Warum fragen wir das: Keine Anhaltspunkte im Unternehmensprofil — klären._ + +## Womit zuerst anfangen (größter Hebel) +- `protection_against_corruption_of_safety_functions` — schließt 2 Anforderung(en): CRA, MaschinenVO +- `exploited_vuln_and_incident_reporting` — schließt 1 Anforderung(en): CRA +- `machine_safety_risk_assessment` — schließt 1 Anforderung(en): MaschinenVO +- `mechanical_safety_and_guards` — schließt 1 Anforderung(en): MaschinenVO +- `operating_instructions_and_safety_information` — schließt 1 Anforderung(en): MaschinenVO + +## Vollständigkeit (ehrlich) +> Identifiziert 1 · bewertet 1 · offen 0 · Unsicherheiten 0 · Begründung ja + +--- +_Der Vertrieb wählt KEIN Regelwerk und interpretiert nichts — er sieht nur dieses Ergebnis. Jede beantwortete Frage aktualisiert das Capability Profile und verkleinert das Delta._ diff --git a/backend-compliance/reference_scenarios/onboarding_advisor_demo.py b/backend-compliance/reference_scenarios/onboarding_advisor_demo.py new file mode 100644 index 00000000..0393433c --- /dev/null +++ b/backend-compliance/reference_scenarios/onboarding_advisor_demo.py @@ -0,0 +1,96 @@ +# ruff: noqa +# mypy: ignore-errors +"""Smart Onboarding Advisor demo — what the frontend shows, automatically (no sales interpretation). + +The user types company + products + certifications + target. The Advisor orchestrates the existing +engines and returns the next best questions, assumptions and measures. Sales sees only the result. +Synthetic, no real names. Non-runtime demo of a runtime step. +Run: cd backend-compliance && PYTHONPATH=. python3 reference_scenarios/onboarding_advisor_demo.py +""" +from __future__ import annotations + +import os +import yaml + +from compliance.onboarding import ( + CapabilityHypothesis, OnboardingInput, ProducedSignal, SignalMapping, SignalVocabularyEntry, + advisor_start, normalize_signals, resolve_for_certifications, silent_intake, +) +from compliance.transition_reasoning import TargetRequirement + +OUT = [] + + +def w(s=""): + OUT.append(s) + + +CRA = yaml.safe_load(open(os.path.join(os.path.dirname(__file__), "..", "knowledge", "transition_patterns", + "transition_pattern_iso27001_to_cra_maschinenvo_v1.yaml"), encoding="utf-8")) +infosec = [a["capability"] for a in CRA["likely_covered"]] +req = [TargetRequirement(capability_id=a["capability"]) for a in CRA["likely_covered"]] +req += [TargetRequirement(capability_id=d["capability"], question_intent=d.get("needed_information", "verify_existence"), + expected_evidence=d.get("expected_evidence", [])) for d in CRA["delta_requirements"]] +covers = {d["capability"]: d.get("covers_targets", []) for d in CRA["delta_requirements"]} +# certificate hypotheses come from the CURATED, capability-centric library (multi-cert merges automatically) +_lib = [CapabilityHypothesis(**h) for h in yaml.safe_load( + open(os.path.join(os.path.dirname(__file__), "..", "knowledge", "certification_hypotheses", "hypotheses.yaml"), encoding="utf-8"))["hypotheses"]] +inp = OnboardingInput(company="synthetisch", industry="machine_builder", + products=["Parkschein-/Schrankensystem"], markets=["EU", "DE"], + certifications=["ISO9001", "ISO27001", "ISO14001", "TISAX"], + known_evidence=["CE process"], target=["CRA"]) +hyp = resolve_for_certifications(inp.certifications, _lib) +# Phase 0 — Signal Producers emit raw dialects -> Normalizer -> one canonical stream -> Silent Pass. +_K = os.path.join(os.path.dirname(__file__), "..", "knowledge", "onboarding") +_vocab = [SignalVocabularyEntry(**v) for v in yaml.safe_load(open(os.path.join(_K, "signal_vocabulary.yaml"), encoding="utf-8"))["signals"]] +_smap = [SignalMapping(**m) for m in yaml.safe_load(open(os.path.join(_K, "intake_signal_map.yaml"), encoding="utf-8"))["mappings"]] +_produced = [ProducedSignal(signal_id="vdp_found", source_type="website", provenance="/.well-known/security.txt"), + ProducedSignal(signal_id="cyclonedx_found", source_type="repository", evidence="sbom", provenance="sbom.cdx.json"), + ProducedSignal(signal_id="cosign_found", source_type="repository", provenance="cosign.pub"), + ProducedSignal(signal_id="risk_assessment_pdf", source_type="document", provenance="risk_assessment.pdf"), + ProducedSignal(signal_id="cloud_hosted", source_type="product"), + ProducedSignal(signal_id="plc_detected", source_type="product")] +_signals = normalize_signals(_produced, _vocab) # raw producer dialects -> ONE canonical signal language +si = silent_intake(_signals, _smap) +res = advisor_start(inp, hyp, req, target_id="CRA", covers_targets=covers, corpus_status={"CRA": "validated"}, + detected_capabilities=si.capability_ids()) + +w("# Smart Onboarding Advisor — was der Nutzer sieht (automatisch, ohne Vertrieb)") +w("") +w("_Eingabe: Unternehmen + Produkte + Zertifizierungen + Ziel. Den Rest macht die Orchestrierung über die bestehenden Engines (Company 2A · RS-005 · Optimization · Completeness). Synthetisch, keine echten Namen._") +w("") +w("## Eingabe") +w("> Zertifizierungen: **%s** · Produkt: **%s** · Ziel: **%s**" % (", ".join(inp.certifications), inp.products[0], ", ".join(inp.target))) +w("") +w("## Phase 0 — Stille Vorbefüllung (BEVOR eine Frage erscheint)") +w("- **Signal Producer (verschiedene Dialekte → ein kanonisches Signal):** %s" % ", ".join("`%s`(%s)" % (p.signal_id, p.source_type) for p in _produced)) +w("> %s" % si.summary) +w("- **Automatisch erkannte Fähigkeiten:** %s" % ", ".join("`%s`" % d.capability for d in si.detected_capabilities)) +w("- **Produktfakten (steuern den Scope):** %s" % ", ".join("`%s=%s`" % (f.key, f.value) for f in si.product_facts)) +w("- **Nachweise bereits in der Hand (kein Upload nötig):** %s" % ", ".join(si.evidence_found)) +w("") +w("## Was wir erkannt haben") +w("> %s" % res.headline) +w("") +w("**Aus Ihren Zertifizierungen abgeleitet (zu bestätigen, nicht automatisch erfüllt):**") +for a in res.inferred_assumptions: + w("- %s" % a.statement) +for r in res.rejected_assumptions: + w("- _%s — %s_" % (r.statement, r.reason)) +w("") +w("## Die wenigen offenen Punkte — nur die nächsten besten Fragen") +for n, q in enumerate(res.next_best_questions, 1): + w("**Frage %d von %d** _(Informationswert %.0f)_" % (n, len(res.next_best_questions), q.information_value)) + w("> %s? — _Warum fragen wir das: %s_" % (q.capability_id.replace("_", " "), q.why)) + w("") +w("## Womit zuerst anfangen (größter Hebel)") +for m in res.top_measures[:5]: + w("- `%s` — schließt %d Anforderung(en): %s" % (m.capability_id, m.leverage, ", ".join(m.closes) or "—")) +w("") +w("## Vollständigkeit (ehrlich)") +w("> %s" % res.completeness_summary) +w("") +w("---") +w("_Der Vertrieb wählt KEIN Regelwerk und interpretiert nichts — er sieht nur dieses Ergebnis. Jede beantwortete Frage aktualisiert das Capability Profile und verkleinert das Delta._") + +print("\n".join(OUT)) diff --git a/backend-compliance/reference_scenarios/reference_scenario_suite_v1.md b/backend-compliance/reference_scenarios/reference_scenario_suite_v1.md new file mode 100644 index 00000000..8679da4c --- /dev/null +++ b/backend-compliance/reference_scenarios/reference_scenario_suite_v1.md @@ -0,0 +1,428 @@ +# Reference Scenario Suite v1 + +> **Kein Doku-Artefakt — die erste Ground Truth / Living Reference Suite.** Erzeugt aus den REALEN deployten Engines (aktueller deployter main) via `reference_scenarios/generate.py`. Jede `Architecture Coverage`-Zelle ist aus dem echten Lauf ABGELEITET; sobald eine Domaene landet, kippt die Zelle automatisch (z. B. Sz2/Environmental UNSUPPORTED -> PASS). Beantwortet dauerhaft: „Ist BreakPilot besser als vor sechs Monaten?" — anhand echter Kundensituationen, nicht LOC. + +Synthetische `Cert->Capability`-Mappings sind als ILLUSTRATIV markiert; die echte Tabelle gehoert Compliance Execution (siehe Master Capability Registry). + +## Szenario 1 — Maschinenbauer mit ISMS + SBOM + Remote Access + +**Frage:** „Was gilt fuer uns, und reicht das?" + +**Input:** Maschine, vernetzt (Remote/Cloud), Firmware, Rolle Hersteller, Maerkte EU/DE; Company: ISMS (ISO27001) + SBOM + Incident Response. + +**Expected Regulatory Map** + +> Für Industrielle Verpackungsmaschine (machinery) — Maschine; vernetzt; Firmware; Rolle: manufacturer; Märkte: EU, DE gelten nach derzeitigem Stand wahrscheinlich: CRA, MaschinenVO, EMV. Unsicher (fehlende Fakten): RED, DataAct, NIS2. Ausgeschlossen: keine. Nicht abgedeckt (Regelkorpus fehlt): keine. Ermittelt: 12 registry-verlinkte Pflichten. Es wurden keine weiteren Regelwerke im aktuellen Korpus identifiziert. + +- **CRA** (Cyber Resilience Act (EU) 2024/2847) — Pflichten: sbom_creation, provide_security_updates, support_period_maintenance, signed_update_integrity, vuln_handling_process, coordinated_vulnerability_disclosure, exploited_vuln_reporting_authorities, user_authentication_required, no_default_credentials, event_logging_security_events, remote_access_attack_surface_min, remote_access_confidentiality_integrity +- **MaschinenVO** (Maschinenverordnung (EU) 2023/1230) — Pflichten: Pflichten für dieses Regelwerk sind noch nicht registry-verlinkt. +- **EMV** (EMV-Richtlinie 2014/30/EU) — Pflichten: Pflichten für dieses Regelwerk sind noch nicht registry-verlinkt. +- _unsicher_ RED — fehlt: Besitzt das Produkt ein Funkmodul (WLAN, Bluetooth, Mobilfunk)? +- _unsicher_ DataAct — fehlt: Erzeugt das vernetzte Produkt nutzbare Produkt-/Nutzungsdaten? +- _unsicher_ NIS2 — fehlt: Unternehmensgröße (Mitarbeiterzahl / Umsatz)?, In welchem Sektor ist das Unternehmen tätig (Anhang I/II)?, Fällt das Unternehmen als wesentliche/wichtige Einrichtung unter NIS2? +- Overlap VULNERABILITY_HANDLING: vuln_handling_process, coordinated_vulnerability_disclosure +- Overlap SECURITY_UPDATES: provide_security_updates, signed_update_integrity +- 1 Nachweis `repo_scan` => 2 Pflichten +- 1 Nachweis `policy` => 5 Pflichten +- 1 Nachweis `ticket` => 3 Pflichten +- 1 Nachweis `test_report` => 3 Pflichten +- 1 Nachweis `config_export` => 6 Pflichten +- 1 Nachweis `pentest` => 3 Pflichten + +**Input — Company Context** _(ILLUSTRATIVES Mapping: ISO27001 -> incident_response, supplier_management, asset_management)_ + +- candidate: cap_patch_management — declared (declaration:maschinenbau) +- candidate: cap_incident_response — inferred (certification:ISO27001) +- candidate: cap_supplier_management — inferred (certification:ISO27001) +- candidate: cap_asset_management — inferred (certification:ISO27001) +- CONFIRMED: cap_sbom_management — confirmed (Nachweis: sbom.json) + +**Expected Interpretation** + +> Auslegung „5 Jahre Updates -> CRA erfuellt" -> **uncertain** +> Ihre Interpretation ist wahrscheinlich unsicher. Kein bekanntes Auslegungsmuster erkannt — bewusst keine Scheinsicherheit. Diese Auslegung betrifft kein Regelwerk Ihrer aktuellen Produkt-Map. + +**Expected RCI** _(CRA-Novelle gegen gespeicherte Baseline)_ + +> affects_product = True — 1 neu, 2 geändert, 0 entfällt, 0 bereits abgedeckt, 2 zu prüfen, 0 nicht relevant. +- cra_new_disclosure_duty -> **new** (fehlende Nachweise: -) +- sbom_creation -> **changed** (fehlende Nachweise: repo_scan) +- vuln_handling_process -> **changed** (fehlende Nachweise: policy, ticket) + +**Expected Unsupported Domains** + +- keine — alle getriggerten Domaenen sind im Korpus + +**Known Gaps:** Interpretation kennt kein CRA-Muster (RS-001) · MaschinenVO/EMV-Pflichten nicht registry-verlinkt (RS-004) · cap/MCAP/Pflicht-Evidence nicht gejoint = Company-Gap (RS-003). + +**Architecture Coverage** + +| Layer | Status | Hinweis | +|---|---|---| +| Company Context | **PASS** | ISO27001 + SBOM + Declaration | +| Product Profile | **PASS** | Maschine, vernetzt, Firmware | +| Navigator | **PASS** | ready_for_scope | +| Scope | **PASS** | CRA/MaschinenVO/EMV + 3 unsicher | +| Regulatory Map | **PASS** | Overlaps + 1-Nachweis-N-Pflichten | +| CRA Obligations | **PASS** | 12 registry-verlinkt | +| MaschinenVO/EMV Obligations | **PARTIAL** | Scope ja, Pflichten nicht verlinkt → RS-004 | +| Interpretation | **PARTIAL** | kein CRA-Muster → RS-001 | +| RCI | **PASS** | 1 neu, 2 geaendert | +| Company Gap | **TODO** | cap↔MCAP↔Pflicht nicht gejoint → RS-003 | +| Environmental | **N/A** | keine Umwelt-Trigger | + +## Szenario 2 — Industriespuelmaschine mit Abwasser/Chemikalien + +**Frage:** „Welche Umweltbereiche sind noch nicht abgedeckt?" + +**Input:** Maschine mit Chemikalien-Dosierung + Abwasserauslass; Umwelt-Trigger gesetzt. + +**Expected Regulatory Map** + +> Für Industriespuelmaschine (machinery) — Maschine; Firmware; Rolle: manufacturer; Märkte: EU, DE gelten nach derzeitigem Stand wahrscheinlich: CRA, MaschinenVO, EMV. Unsicher (fehlende Fakten): RED, DataAct, NIS2. Ausgeschlossen: keine. Nicht abgedeckt (Regelkorpus fehlt): environment_water, chemicals, energy_resources. Ermittelt: 10 registry-verlinkte Pflichten. Es wurden keine weiteren Regelwerke im aktuellen Korpus identifiziert. + +- **CRA** (Cyber Resilience Act (EU) 2024/2847) — Pflichten: sbom_creation, provide_security_updates, support_period_maintenance, signed_update_integrity, vuln_handling_process, coordinated_vulnerability_disclosure, exploited_vuln_reporting_authorities, user_authentication_required, no_default_credentials, event_logging_security_events +- **MaschinenVO** (Maschinenverordnung (EU) 2023/1230) — Pflichten: Pflichten für dieses Regelwerk sind noch nicht registry-verlinkt. +- **EMV** (EMV-Richtlinie 2014/30/EU) — Pflichten: Pflichten für dieses Regelwerk sind noch nicht registry-verlinkt. +- _unsicher_ RED — fehlt: Besitzt das Produkt ein Funkmodul (WLAN, Bluetooth, Mobilfunk)? +- _unsicher_ DataAct — fehlt: Erzeugt das vernetzte Produkt nutzbare Produkt-/Nutzungsdaten? +- _unsicher_ NIS2 — fehlt: Unternehmensgröße (Mitarbeiterzahl / Umsatz)?, In welchem Sektor ist das Unternehmen tätig (Anhang I/II)?, Fällt das Unternehmen als wesentliche/wichtige Einrichtung unter NIS2? +- Overlap VULNERABILITY_HANDLING: vuln_handling_process, coordinated_vulnerability_disclosure +- Overlap SECURITY_UPDATES: provide_security_updates, signed_update_integrity +- 1 Nachweis `policy` => 5 Pflichten +- 1 Nachweis `ticket` => 3 Pflichten +- 1 Nachweis `test_report` => 3 Pflichten +- 1 Nachweis `config_export` => 4 Pflichten + +**Expected Unsupported Domains** + +- `environment_water` (Trigger: discharges_to_wastewater) -> Abwasser-/Gewässerrecht (z. B. AbwV, WRRL) — noch nicht im Korpus. +- `chemicals` (Trigger: uses_cleaning_chemicals) -> Chemikalienrecht (REACH/CLP/Detergenzien/Biozide) — noch nicht im Korpus. +- `energy_resources` (Trigger: consumes_energy_or_water) -> Energie-/Ökodesign-Recht — noch nicht im Korpus. + +**Expected Interpretation** _(Umwelt -> bewusst nicht bewertet)_ + +> Ihre Interpretation ist wahrscheinlich unsicher. Kein bekanntes Auslegungsmuster erkannt — bewusst keine Scheinsicherheit. Für environment_water, chemicals liegt noch kein Regelkorpus vor — diese Aspekte werden nicht bewertet (future_corpus_needed). + +**Known Gaps:** Abwasser/Chemikalien/Energie sind `unsupported_domain` — Environmental Corpus fehlt (RS-002). + +**Architecture Coverage** + +| Layer | Status | Hinweis | +|---|---|---| +| Product Profile | **PASS** | Maschine + Umwelt-Komponenten | +| Scope | **PASS** | | +| Regulatory Map | **PASS** | CRA/MaschinenVO/EMV | +| Environmental (Abwasser/Chemikalien/Energie) | **UNSUPPORTED** | ehrlich „noch nicht im Korpus" → RS-002 | +| Interpretation (Umwelt) | **PARTIAL** | future_corpus_needed statt Scheinsicherheit | + +## Szenario 3 — ISO27001-zertifiziertes Unternehmen + +**Frage:** „Welche Capabilities sind inferred, declared oder confirmed?" + +_ILLUSTRATIVES Mapping: ISO27001 -> incident_response, supplier_management, asset_management_ + +**Expected Company Capability Profile** _(4-Zustands-Trust-Model)_ + +- cap_incident_response — **inferred** (Quelle: certification:ISO27001) +- cap_supplier_management — **inferred** (Quelle: certification:ISO27001) +- cap_asset_management — **inferred** (Quelle: certification:ISO27001) +- cap_patch_management — **confirmed** (Nachweis: patch-policy.pdf) + +**Expected Master Capability Registry** _(computed confidence, policy-versioniert)_ + +- ISO27001 *supports* MCAP-00001 -> inferred/low (policy capability-policy-v0) — computed, nicht gespeichert +- ir-runbook.pdf *confirms* MCAP-00001 -> confirmed/high — nur echtes Artefakt erreicht confirmed + +**Known Gaps:** `cap_*` (Company 2A) und `MCAP-*` (Registry) sind noch nicht verlinkt (RS-003). + +**Architecture Coverage** + +| Layer | Status | Hinweis | +|---|---|---| +| Company Context | **PASS** | ISO27001 + Declaration + Evidence | +| Trust-State (declared/inferred/confirmed) | **PASS** | Zertifizierung nie confirmed | +| Master Capability Registry | **PASS** | computed confidence, policy-versioniert | +| cap ↔ MCAP Linking | **TODO** | zwei Vokabulare unverbunden → RS-003 | + +## Szenario 4 — Transition (RS-005 fährt JEDEN Knowledge Pattern) + +_Genericity-Beweis: derselbe Algorithmus trägt jeden Transition Knowledge Pattern, nicht nur den CRA._ + +**ISO/IEC 27001 → TISAX** _(TP-ISMS-TISAX-v1, status=draft)_ +> 13 zu klären, 0 bereits abgedeckt, 8 vermutlich vorhanden, 5 fehlt, 0 n/a, 0 nicht im Korpus. +- Delta zuerst (HIGH): data_protection_processing_on_behalf, prototype_protection, tisax_assessment_via_enx, tisax_label_scope_selection, vda_isa_self_assessment +- vermutlich abgedeckt: information_security_management, access_control_and_authentication, asset_and_configuration_management, incident_management, supplier_security, cryptography, physical_security, security_awareness_training +- Pattern getragen: **ja** (13 caps → 13 coverage + 13 requests) + +**ISO/IEC 27001 → Cyber Resilience Act** _(TP-ISO27001-CRA-v1, status=reviewed)_ +> 17 zu klären, 0 bereits abgedeckt, 8 vermutlich vorhanden, 9 fehlt, 0 n/a, 0 nicht im Korpus. +- Delta zuerst (HIGH): ce_conformity_assessment_and_technical_documentation, coordinated_vulnerability_disclosure, exploited_vuln_and_incident_reporting, product_cyber_risk_assessment, public_security_advisories, sbom_creation, secure_by_default_no_default_credentials, secure_signed_update_distribution, security_update_support_period +- vermutlich abgedeckt: incident_management, technical_vulnerability_management, supplier_security, access_control_and_authentication, cryptography, security_logging_and_monitoring, secure_development_lifecycle, asset_and_configuration_management +- Pattern getragen: **ja** (17 caps → 17 coverage + 17 requests) + +**ISO 9001 → Cyber Resilience Act** _(TP-ISO9001-CRA-v1, status=draft)_ +> 13 zu klären, 0 bereits abgedeckt, 3 vermutlich vorhanden, 10 fehlt, 0 n/a, 0 nicht im Korpus. +- Delta zuerst (HIGH): access_control_and_authentication, ce_conformity_assessment_and_technical_documentation, coordinated_vulnerability_disclosure, exploited_vuln_and_incident_reporting, product_cyber_risk_assessment, sbom_creation, secure_development_lifecycle, secure_signed_update_distribution, security_update_support_period, technical_vulnerability_management +- vermutlich abgedeckt: document_and_change_control, supplier_evaluation, release_and_approval_process +- Pattern getragen: **ja** (13 caps → 13 coverage + 13 requests) + +**Architecture Coverage** + +| Layer | Status | Hinweis | +|---|---|---| +| Transition ISOIEC27001→TISAX | **PASS** | draft · 5 HIGH-Delta + 8 zu bestätigen | +| Transition ISOIEC27001→Cyber Resilience Act | **PASS** | reviewed · 9 HIGH-Delta + 8 zu bestätigen | +| Transition ISO9001→Cyber Resilience Act | **PASS** | draft · 10 HIGH-Delta + 3 zu bestätigen | +| RS-005.1 Renderer (Fragetext) | **TODO** | verschoben — Engine liefert nur Requests | + +## Reference Transition Scenarios (RTS) — kanonische Regression (Soll/Ist) + +_Anonymisierte Archetypen (KEINE Firmennamen). Jeder RTS pinnt ein Expected Outcome; jeder Commit muss es reproduzieren (identisch oder besser). Data Act = `uncertain`, nie fix „gilt/gilt-nicht"._ + +**RTS-001** — Automotive supplier with a mature ISMS — embedded electronics + software, CE products, OEM supply chain +> Start TISAX+ISO27001 → CRA. 17 zu klären, 0 bereits abgedeckt, 8 vermutlich vorhanden, 9 fehlt, 0 n/a, 0 nicht im Korpus. +- Expected Delta erfüllt: **ja** (7/7 Soll-Delta in der Ist-Lücke) +- Expected likely_covered erfüllt: **ja** +- Data Act: Engine sagt **uncertain** (Soll: uncertain; nie asserted) → ok +- MaschinenVO: Soll **uncertain** (Komponente, deciding: is_safety_component) → Engine asserted nicht: ok + +**RTS-002** — Classic machine builder with only a QMS — precision systems, CE products, no ISMS +> Start ISO9001 → CRA. 13 zu klären, 0 bereits abgedeckt, 3 vermutlich vorhanden, 10 fehlt, 0 n/a, 0 nicht im Korpus. +- Expected Delta erfüllt: **ja** (9/9 Soll-Delta in der Ist-Lücke) +- Expected likely_covered erfüllt: **ja** +- Data Act: Engine sagt **uncertain** (Soll: uncertain; nie asserted) → ok +- MaschinenVO **gilt** (is_machine): Safety-Delta machine_safety_risk_assessment, mechanical_safety_and_guards, operating_instructions_and_safety_information — **geringe Konvergenz ohne ISMS** (RS-004 reg-map-Gate offen) + +**RTS-003** — Machine builder with an ISMS and networked products — connected machines that may generate usage data +> Start ISO27001 → CRA. 17 zu klären, 0 bereits abgedeckt, 8 vermutlich vorhanden, 9 fehlt, 0 n/a, 0 nicht im Korpus. +- Expected Delta erfüllt: **ja** (7/7 Soll-Delta in der Ist-Lücke) +- Expected likely_covered erfüllt: **ja** +- Data Act: Engine sagt **uncertain** (Soll: uncertain; nie asserted) → ok +- MaschinenVO **gilt** (is_machine): 4/4 Safety-Delta in der Ist-Lücke (Convergence-Pattern) → ok +- Konvergenz CRA∩MaschinenVO: 4/4 erwartete Multi-Target-Caps → ok (4 von 12 Capabilities decken >= 2 Regelwerke gleichzeitig ab (CRA + MaschinenVO).) + +**Architecture Coverage** + +| Layer | Status | Hinweis | +|---|---|---| +| RTS-001 (TISAX→CRA+MaschVO) | **PASS** | 7/7 Delta-Soll · likely_covered ok · DataAct=uncertain · MaschVO=uncertain(ok) | +| RTS-002 (ISO9001→CRA+MaschVO) | **PASS** | 9/9 Delta-Soll · likely_covered ok · DataAct=uncertain · MaschVO=applies (geringe Konvergenz, kein ISMS) | +| RTS-003 (ISO27001→CRA+MaschVO) | **PASS** | 7/7 Delta-Soll · likely_covered ok · DataAct=uncertain · MaschVO=applies 4/4 Safety-Delta · Konvergenz ok | + +## Regulatory Convergence — CRA + MaschinenVO (Cross-Regulation Capability Mapping) + +_Der USP: welche Capability deckt MEHRERE Regelwerke gleichzeitig? (Convergence Pattern, RTS-003-Archetyp.)_ + +**Cross-Regulation Capability Mapping (Delta):** 4 von 12 Capabilities decken >= 2 Regelwerke gleichzeitig ab (CRA + MaschinenVO). + +**Konvergenz — diese neuen Maßnahmen decken BEIDE Regelwerke gleichzeitig:** +- `ce_conformity_assessment_and_technical_documentation` +- `product_cyber_risk_assessment` +- `protection_against_corruption_of_safety_functions` +- `secure_signed_update_distribution` + +**Pro Regelwerk benötigt (Delta):** CRA=9, MaschinenVO=7 + +**Kundensatz:** „Von den 12 neuen Maßnahmen erfüllen 4 gleichzeitig CRA und MaschinenVO." (heute liefert das praktisch kein Tool) + +**Architecture Coverage** + +| Layer | Status | Hinweis | +|---|---|---| +| Regulatory Convergence Pattern | **PASS** | 2 Targets, 12 Delta-Capabilities | +| Cross-Regulation Capability Mapping | **PASS** | 4 von 12 Capabilities decken >= 2 Regelwerke gleichzeitig ab (CRA + MaschinenVO). | + +## Regulatory Optimization — größter regulatorischer Hebel zuerst + +_Dieselbe Berechnung wie die GAP-Analyse, anderer Renderer: das **Capability Delta** (RS-005) wird nach **regulatorischem Hebel** priorisiert (eine Maßnahme schließt N Regelwerke gleichzeitig). Welt-1: % über die IDENTIFIZIERTEN Anforderungen, kein Compliance-Urteil._ + +**Kompression:** 16 identifizierte Anforderungen aus 2 Regelwerken -> 12 Massnahmen (Ø Hebel 1.3). + +**Top-Maßnahmen nach regulatorischem Hebel (Roadmap):** + +| # | Maßnahme | Hebel | deckt | kumuliert | +|---|---|---|---|---| +| 1 | `ce_conformity_assessment_and_technical_documentation` | **2** | CRA+MaschinenVO | 2/16 (12%) | +| 2 | `product_cyber_risk_assessment` | **2** | CRA+MaschinenVO | 4/16 (25%) | +| 3 | `protection_against_corruption_of_safety_functions` | **2** | CRA+MaschinenVO | 6/16 (38%) | +| 4 | `secure_signed_update_distribution` | **2** | CRA+MaschinenVO | 8/16 (50%) | +| 5 | `coordinated_vulnerability_disclosure` | **1** | CRA | 9/16 (56%) | +| 6 | `exploited_vuln_and_incident_reporting` | **1** | CRA | 10/16 (62%) | + +**Managementsatz:** „Wenn Sie zuerst diese 5 Maßnahmen umsetzen, schließen Sie 9 von 16 identifizierten Anforderungen (56%) — höchster regulatorischer Hebel." (Hebel skaliert mit jedem weiteren Regelwerk/Convergence-Pattern.) + +_Eine Wahrheit, zwei Renderer: dasselbe Capability Delta liefert dem Auditor **Fragen** (Interview) und dem GF **Maßnahmen** (Roadmap)._ + +**Architecture Coverage** + +| Layer | Status | Hinweis | +|---|---|---| +| Capability Delta Engine (RS-005) | **PASS** | ein Delta, mehrere Renderer | +| Roadmap/Management Renderer (Hebel) | **PASS** | 16 identifizierte Anforderungen aus 2 Regelwerken -> 12 Massnahmen (Ø Hebel 1.3). | +| Budget-Priorisierung | **PASS** | Top-5 → 56% der identifizierten Anforderungen | + +## Implementation Playbook — wie komme ich dort hin? (Berater-Renderer) + +_Nach „was fehlt?" (Delta) und „womit anfangen?" (Hebel) die nächste Ebene: **wie umsetzen?** Pro Maßnahme eine komplette Reise aus kuratiertem Wissen + Hebel + (injizierten) Execution-Links. Inhalt ist der Engpass, nicht die Software._ + +**Reise pro Maßnahme (aus der Roadmap):** 2 von 12 Maßnahmen haben ein Playbook; 10 brauchen noch Inhalt (Knowledge Acquisition). + +**Beispielreise — `sbom_creation`** _(draft, schließt CRA)_ +> **Warum?** Der CRA verlangt von Herstellern, die Komponenten ihres Produkts zu identifizieren und zu dokumentieren (Schwachstellenbehandlung, Annex I Teil II). Eine SBOM ist das maschinenlesbare Inventar aller (auch transitiven) Software-Bestandteile mit Version und Lizenz. Ohne SBOM kann niemand verlässlich sagen, welche Produkte von einer neuen Schwachstelle (CVE) betroffen sind — SBOM ist damit die Voraussetzung für Schwachstellenüberwachung, Security-Updates und Meldepflichten. +- **Tools:** CycloneDX (Format, OWASP), SPDX (Format, Linux Foundation), Syft (Generator, Container/Filesystem), cdxgen (Generator, Multi-Ökosystem), Trivy (Generator + Scan), OWASP Dependency-Track (Verwaltung + kontinuierliche Überwachung) +- **Prozess:** Format festlegen → Automatisch im Build erzeugen → Transitive Abhängigkeiten + Versionen + Lizenzen erfassen → Pro Release versionieren und ablegen → An Schwachstellenüberwachung anbinden → Release-Gate setzen +- **Nachweise:** Maschinenlesbare SBOM (CycloneDX/SPDX) je ausgelieferter Produktversion, CI-Job-Konfiguration, die die SBOM automatisch erzeugt, Dependency-Track-Projekt (oder gleichwertig) mit laufender Überwachung +- **Wie andere es tun:** Verbreitete Praxis: CycloneDX automatisch in der CI via Syft/cdxgen erzeugen und nach OWASP Dependency-Track pushen, das kontinuierlich gegen neue CVEs prüft. Reifere Organisationen gaten Releases auf das Vorhandensein einer SBOM und teilen sie auf Anfrage mit Kunden/Behörden. + +**Roadmap → Implementation (Top-Maßnahmen nach Hebel):** + +| Maßnahme | Hebel | schließt | Playbook | +|---|---|---|---| +| `ce_conformity_assessment_and_technical_documentation` | 2 | CRA+MaschinenVO | **fehlt (Inhalt)** | +| `product_cyber_risk_assessment` | 2 | CRA+MaschinenVO | **fehlt (Inhalt)** | +| `protection_against_corruption_of_safety_functions` | 2 | CRA+MaschinenVO | **fehlt (Inhalt)** | +| `secure_signed_update_distribution` | 2 | CRA+MaschinenVO | **fehlt (Inhalt)** | +| `coordinated_vulnerability_disclosure` | 1 | CRA | ✓ draft | +| `exploited_vuln_and_incident_reporting` | 1 | CRA | **fehlt (Inhalt)** | + +_Derselbe Capability-Strang, neuer Renderer: aus Diagnose wird Beratung. Die `fehlt`-Einträge sind der ehrliche Content-Backlog (höchster Hebel zuerst befüllen)._ + +**Architecture Coverage** + +| Layer | Status | Hinweis | +|---|---|---| +| Implementation Playbook Renderer | **PASS** | Reise pro Capability (why/tools/process/evidence/controls) | +| Roadmap → Playbook (Verkettung) | **PASS** | 2/12 Maßnahmen mit Playbook | +| Playbook-Inhalt (Knowledge) | **TODO** | 10 Capabilities brauchen noch Inhalt | + +## Knowledge Production — Playbook-Entwürfe automatisch assemblieren + +_Der Engpass ist nicht Content, sondern Wissensproduktion. Der Corpus wird nicht von Hand geschrieben, sondern deterministisch aus vorhandenen Daten (Transition Pattern + Leverage + injizierte Controls) vorbereitet — dann fachlich kuratiert (wie Gesetz→Parser→Obligation→Review)._ + +**Aus 1 Pattern → 12 Playbook-Entwürfe** (`status: draft_generated`): eigene Felder (Warum/schließt/Nachweise) aus den Daten gefüllt, der Experte ergänzt nur Tools/Prozess/How-others. + +**Beispiel-Entwurf — `sbom_creation`** _(draft_generated)_ +- **Warum** (aus Pattern): CRA requires an SBOM; MaschinenVO does not. +- **schließt** CRA · **Nachweise** sbom +- **Provenance:** why←transition_pattern:why_asked, closes_regulations←leverage:covers_targets, expected_evidence←transition_pattern:expected_evidence +- **TODO (Experte/Offline-Propose):** tools, process_steps, how_others_do_it + +_So reviewt der Experte 12 Entwürfe statt 12 Playbooks zu schreiben. Derselbe Generator bereitet später ISO14001-/IATF-Entwürfe vor, sobald der Corpus da ist._ + +**Architecture Coverage** + +| Layer | Status | Hinweis | +|---|---|---| +| Playbook Draft Generator (deterministisch) | **PASS** | 12 Entwürfe aus 1 Pattern, kein LLM im Kern | +| Provenance + TODO + Freigabestatus | **PASS** | draft_generated→reviewed→validated→proven | +| Draft-Generatoren neue Domänen (Phase A) | **TODO** | Transition-/Reference-Scenario-Drafts | + +## Knowledge Intake — Impact zuerst, Extraktion später + +_Vor dem Parser: ein neues Dokument NUR einordnen und seinen Impact auf den bestehenden Wissensbestand bestimmen. „Von N Dokumenten verändern wenige tatsächlich unser Wissen." Deterministisch, keine Extraktion, kein LLM._ + +| Dokument | Impact | betrifft | Empfehlung | +|---|---|---|---| +| ENISA CRA SBOM-FAQ | **high** | 14C·2PB·3RTS·2Obl | Gezielter Review priorisieren | +| EU Umwelt-Leitfaden | **new_domain** | neue Domäne | Neue Domäne | +| Marketing-Blog | **none** | 0C·0PB·0RTS·0Obl | Wahrscheinlich ignorierbar | + +**Beispiel-Knowledge-Package** (`ENISA CRA SBOM-FAQ`): Betrifft 14 Capabilities, 2 Playbooks, 0 Patterns, 3 Reference Scenarios, 2 Obligations; keine neue Domäne. + +_So entsteht bei jedem neuen Dokument eine Impact-Analyse statt „200 Seiten PDF" — Targeted Updating statt Schreiben._ + +**Architecture Coverage** + +| Layer | Status | Hinweis | +|---|---|---| +| Knowledge Intake (Klassifikation+Impact) | **PASS** | 6 Regelwerke / 32 Capabilities im Index | +| Impact-Triage (HIGH/LOW/NONE/new_domain) | **PASS** | 3 Beispiel-Dokumente korrekt eingeordnet | +| Regelwerk-ID-Normalisierung | **TODO** | CRA vs Cyber Resilience Act vereinheitlichen | + +## Regulatory Completeness — was wir bewerten konnten, und was bewusst nicht + +_Interne Qualitätsmaschine (KEIN Confidence-Score): trennt IDENTIFIZIERT von BEWERTET und begründet jede Lücke. Keine Prozentzahl — auditierbar und ehrlich: „Wir zeigen auch, was wir noch nicht wissen und warum."_ + +**Identifiziert 5 · bewertet 2 · offen 3 · Unsicherheiten 1 · Begründung ja** + +> Für dieses Produkt konnten wir 2 von 5 identifizierten regulatorischen Domänen vollständig bewerten. 3 weitere sind noch nicht Bestandteil des validierten Korpus bzw. anwendungsunsicher und wurden deshalb bewusst nicht bewertet. + +- **Bewertet:** CRA, MaschinenVO (128 Pflichten) +- **Offen (jeweils begründet):** + - `DataAct` — generates_usage_data = unbekannt `[query_required]` → Rückfrage: `generates_usage_data` + - `EMV` — nicht im validierten Korpus `[future_corpus]` + - `Environmental` — nicht im validierten Korpus `[future_corpus]` +- **Annahmen:** Funkmodul=nein, personenbezogene Nutzungsdaten=nein + +_Sobald der Umwelt-Korpus (ISO 14001 etc.) landet, kippt `Environmental` automatisch von offen auf bewertet — die Completeness Engine dokumentiert den Fortschritt je Domäne._ + +**Architecture Coverage** + +| Layer | Status | Hinweis | +|---|---|---| +| Regulatory Completeness (auditierbar) | **PASS** | Identifiziert 5 · bewertet 2 · offen 3 · Unsicherheiten 1 · Begründung ja | +| Begründete Ausschlüsse (Korpus/Anwendbarkeit) | **PASS** | 3 Ausschlüsse, alle mit Grund | +| Fortschritts-Doku je Domäne | **PASS** | Environmental offen→validated bei Korpus-Landung | + +## Domain Knowledge Program v1 — Reifegrad je Domäne (reproduzierbarer KPI) + +_Engpass = Domänenmodellierung. Jede Domäne läuft durch DIESELBE 7-Stufen-Produktionsstraße (Domain Model → Requirement Sources → Capability Registry → Transition Patterns → Playbooks → Reference Scenarios → Completeness). Reifegrad aus dem ECHTEN Korpus abgeleitet (computed-not-stored), keine Marketingzahl. Einstieg über Industry, nicht Regelwerk._ + +| Rank | Domäne | Reifegrad (Sources modelliert) | modelliert/total | Korpus TP·PB·RTS | +|---|---|---|---|---| +| 1 | **Industrial Automation Domain** | `████░░░░░░` 43% | 3/7 | 3·2·3 | +| 2 | **Environmental Domain** | `░░░░░░░░░░` 0% | 0/6 | 0·0·0 | +| 3 | **Automotive Domain** | `██░░░░░░░░` 17% | 1/6 | 1·0·0 | +| 4 | **Medical Domain** | `██░░░░░░░░` 20% | 1/5 | 3·2·3 | +| 5 | **Energy Domain** | `██░░░░░░░░` 25% | 1/4 | 3·2·3 | + +_Industry-Einstieg + ETO-Hypothese: jede Domäne kennt ihre typischen Sources + Zertifikate → vor dem Onboarding „diese Prozesswelt ist wahrscheinlich vorhanden" (Hypothese, nie Wahrheit; speist Company 2A als `inferred`). Backlog nach Kundennutzen, KPI nach echtem Korpusstand — beides bewusst getrennt._ + +**Architecture Coverage** + +| Layer | Status | Hinweis | +|---|---|---| +| Domain Knowledge Program (7-Stufen-Produktionsstraße) | **PASS** | 5 Domänen im Backlog, Industrial Automation #1 | +| Reifegrad-KPI (computed-not-stored) | **PASS** | aus echtem Korpus abgeleitet (TP/PB/RTS je Domäne) | +| Regelwerk-ID-Normalisierung (Domain Vocabulary) | **PASS** | Aliase aus `vocabulary/regulations.yaml`, nicht mehr hartkodiert | + +## Transition Coverage — die Transition ist die Wissenseinheit (Operational Knowledge) + +_Der Kunde kauft nicht „EMV-Domain", sondern „wir haben ISO 9001 — helfen Sie uns beim CRA". Die Wissenseinheit ist die TRANSITION (nicht das Gesetz). Status je Transition aus dem echten Pattern-Korpus abgeleitet (computed-not-stored). Drei Ebenen: Regulatory → **Operational (hier, größter Differenzierer)** → Verification (Vision V2)._ + +| Prio | Transition | Status | +|---|---|---| +| ⭐⭐⭐⭐⭐ | `ISO27001 → CRA` | ✅ reviewed | +| ⭐⭐⭐⭐⭐ | `ISO9001 → CRA` | 🟡 Draft | +| ⭐⭐⭐⭐⭐ | `ISO9001 → MaschinenVO` | ⚪ nicht begonnen | +| ⭐⭐⭐⭐ | `IEC62443 → CRA` | ⚪ nicht begonnen | +| ⭐⭐⭐⭐ | `TISAX → CRA` | ⚪ nicht begonnen | +| ⭐⭐⭐⭐ | `ISO27001 → NIS2` | ⚪ nicht begonnen | +| ⭐⭐⭐⭐ | `IEC62443 → NIS2` | ⚪ nicht begonnen | +| ⭐⭐⭐ | `ISO14001 → Umweltrecht` | ⚪ nicht begonnen | + +**Größte Lücke (Track B als Nächstes):** `ISO9001 → MaschinenVO` (⭐⭐⭐⭐⭐) — höchstnachgefragte Transition OHNE Pattern. Stärkerer Produktindikator als „EMV 30% modelliert". + +**Architecture Coverage** + +| Layer | Status | Hinweis | +|---|---|---| +| Transition Coverage (Operational Knowledge) | **PASS** | 2 von 8 Top-Transitionen mit Pattern | +| Wissenseinheit = Transition (nicht Gesetz) | **PASS** | verkauft wird der Übergang, z. B. ISO9001→CRA | +| 3 Ebenen Regulatory→Operational→Verification | **PASS** | Operational = größter Differenzierer (ADR-010) | + +## Gaps → Epics (Backlog — nur erfasst, NICHT implementiert) + +| Epic | Titel | schliesst Coverage-Luecke | +|---|---|---| +| RS-001 | Interpretation Pattern Library | Sz1 Interpretation PARTIAL -> PASS (CRA-Muster) | +| RS-002 | Environmental Corpus (Pilotdomaene) | Sz2 Environmental UNSUPPORTED -> PASS | +| RS-003 | Capability Linking (cap↔MCAP) + Company-Gap | Sz1/Sz3 Company Gap TODO -> PASS | +| RS-004 | MaschinenVO/EMV Registry Linking | Sz1/Sz2 MaschinenVO/EMV PARTIAL -> PASS | + +## Suite-Status (Roll-up) + +- Coverage-Zellen gesamt: **50** +- PASS: **39** · PARTIAL: 3 · UNSUPPORTED: 1 · TODO: 6 · N/A: 1 · NEEDS_FACTS: 0 +- Fortschritt = PASS-Anteil steigt, wenn Epics RS-001…004 landen (objektiver Maßstab, kein LOC). diff --git a/backend-compliance/tests/test_architecture_stability_kpi.py b/backend-compliance/tests/test_architecture_stability_kpi.py new file mode 100644 index 00000000..a47fe042 --- /dev/null +++ b/backend-compliance/tests/test_architecture_stability_kpi.py @@ -0,0 +1,79 @@ +"""Architecture Stability + Knowledge Velocity KPI — Phase Ω guardrail. + +This is not a feature test; it is a LIVING GUARDRAIL. The day someone integrates a Requirement Source +that needs a new runtime class or a new pipeline (i.e. NOT data-only), `test_every_source_is_data_only` +fails — surfacing the exact moment the architecture stopped being general. That is the whole point of +Phase Ω: measure where it breaks under real domain knowledge. +""" + +from __future__ import annotations + +import os +import subprocess +import sys + +import yaml + + +def _ledger(): + p = os.path.join(os.path.dirname(__file__), "..", "knowledge", "architecture_stability", "integration_ledger.yaml") + return yaml.safe_load(open(p, encoding="utf-8")) + + +def _run(): + root = os.path.join(os.path.dirname(__file__), "..") + r = subprocess.run( + [sys.executable, "reference_scenarios/architecture_stability_kpi.py"], + cwd=root, env={**os.environ, "PYTHONPATH": "."}, capture_output=True, text=True, + ) + assert r.returncode == 0, r.stderr + return r.stdout + + +def test_every_source_is_data_only_and_zero_runtime(): + # GUARDRAIL: every integrated source must cost 0 runtime classes, no new pipeline, data-only. + # If a future domain breaks this, fix the architecture or record the break honestly — do not + # weaken this assertion to make it pass. + for s in _ledger()["sources"]: + assert s["new_runtime_classes"] == 0, s["source"] + assert s["new_pipeline"] is False, s["source"] + assert s["integration_kind"] == "data_only", s["source"] + + +def test_kpis_reported_at_full_stability(): + out = _run() + n = len(_ledger()["sources"]) + assert "Architecture Stability: %d/%d = 100%%" % (n, n) in out + assert "Knowledge Velocity: %d/%d = 100%%" % (n, n) in out + + +def test_capability_types_column_and_non_cyber_generality(): + out = _run() + # the third KPI column (capability-model granularity Frühindikator) is present and populated + assert "neue Capability-Typen" in out + assert "Capability-Modell-Frühindikator" in out + # the first non-cyber domain is recorded and carried the pipeline 0/0 + assert "non_cyber" in out + assert "Generalität über Cyber hinaus" in out + # every ledger source carries a capability-type count + for s in _ledger()["sources"]: + assert s["new_capability_types"] >= 1, s["source"] + + +def test_pipeline_functions_are_one_time_infrastructure(): + out = _run() + assert "EINMALIG (jetzt eingefroren)" in out + assert "journey_matcher" in out and "letzte architektonische Baustein" in out + + +def test_three_knowledge_layers_present(): + out = _run() + for layer in ["Beschreibend", "Transformation", "Produktion"]: + assert layer in out + + +def test_three_success_questions_present(): + out = _run() + assert "Musste für eine neue Domäne Runtime-Code geändert werden?" in out + assert "Knowledge Velocity" in out + assert "Architecture Stability" in out diff --git a/backend-compliance/tests/test_automotive_convergence_stress_test.py b/backend-compliance/tests/test_automotive_convergence_stress_test.py new file mode 100644 index 00000000..e0e85c30 --- /dev/null +++ b/backend-compliance/tests/test_automotive_convergence_stress_test.py @@ -0,0 +1,62 @@ +"""Automotive convergence stress test — the SAME capability from many sources (Phase Ω #2). + +Pins the convergence property: a realistic multi-certified supplier (ISO 9001 + IATF 16949 + TISAX + +ASPICE + CSMS + SUMS) developing an ECU for OEM X feeds the SAME capability from many overlapping +Requirement Sources, and the model stays stable (0 runtime). Checks the three new measurements: +Capability Convergence (sources x distinct types), Existing-vs-New reuse, and Business Leverage. +""" + +from __future__ import annotations + +import os +import subprocess +import sys + + +def _run(): + root = os.path.join(os.path.dirname(__file__), "..") + r = subprocess.run( + [sys.executable, "reference_scenarios/automotive_convergence_stress_test.py"], + cwd=root, env={**os.environ, "PYTHONPATH": "."}, capture_output=True, text=True, + ) + assert r.returncode == 0, r.stderr + return r.stdout + + +def test_runs_end_to_end_multi_source(): + out = _run() + assert "Automotive Convergence Stress Test" in out + assert "27 distinct geforderte Capabilities" in out + assert "0 neue Runtime-Klassen" in out + + +def test_capability_convergence_ranks_the_most_shared_cap_first(): + out = _run() + # the most convergent capability is fed by 4 sources across 3 distinct source types + assert "| `technical_vulnerability_management` | 4 | 3 |" in out + assert "| `secure_signed_update_distribution` | 4 | 2 |" in out + + +def test_existing_vs_new_reuse_signal(): + out = _run() + # 13 of 27 required caps reuse existing cyber/environmental MCAPs (registry converging) + assert "Reuse aus Cyber/Umwelt): 13/27 = 48%" in out + assert "Registry zu konvergieren" in out + + +def test_business_leverage_is_markets_plus_regulation(): + out = _run() + assert "Business Leverage" in out + assert "öffnet den **OEM-Markt**" in out + + +def test_recommends_registry_analysis_not_next_domain(): + out = _run() + assert "innehalten und die Registry analysieren" in out + assert "Plattformkern" in out + + +def test_no_real_company_names(): + out = _run().lower() + for name in ["eto", "owis", "winterhalter", "bmw", "volkswagen", "bosch"]: + assert name not in out diff --git a/backend-compliance/tests/test_capability_convergence_explanation.py b/backend-compliance/tests/test_capability_convergence_explanation.py new file mode 100644 index 00000000..3c876187 --- /dev/null +++ b/backend-compliance/tests/test_capability_convergence_explanation.py @@ -0,0 +1,48 @@ +"""Capability Convergence Explanation — why the registry converges (Phase Ω, understand the core). + +Pins the three derived views: the why-converge domain matrix (cross-domain core caps + a reason), the +family reduction (~60-70 MCAPs collapse to a small set of families), and the COMPUTED Core-vs-Domain +split (Core recurs across independent domains+types; Domain stays in one — which Medical made obvious). +""" + +from __future__ import annotations + +import os +import subprocess +import sys + + +def _run(): + root = os.path.join(os.path.dirname(__file__), "..") + r = subprocess.run( + [sys.executable, "reference_scenarios/capability_convergence_explanation.py"], + cwd=root, env={**os.environ, "PYTHONPATH": "."}, capture_output=True, text=True, + ) + assert r.returncode == 0, r.stderr + return r.stdout + + +def test_runs_and_explains_why(): + out = _run() + assert "WARUM konvergieren" in out + assert "Universeller Prozess" in out # a reason, not just a count + + +def test_cross_domain_core_caps_in_matrix(): + out = _run() + for cap in ["secure_signed_update_distribution", "sbom_creation", "technical_vulnerability_management"]: + assert cap in out + + +def test_families_reduce_the_registry(): + out = _run() + assert "Capability Families" in out + assert "reduzieren sich auf" in out + for fam in ["Risk", "Update", "Identity & Access", "Inventory & Composition"]: + assert fam in out + + +def test_core_vs_domain_is_computed(): + out = _run() + assert "BERECHNETE Eigenschaft" in out + assert "**Core (" in out and "**Domain (" in out diff --git a/backend-compliance/tests/test_certification_hypotheses.py b/backend-compliance/tests/test_certification_hypotheses.py new file mode 100644 index 00000000..08d5f0d5 --- /dev/null +++ b/backend-compliance/tests/test_certification_hypotheses.py @@ -0,0 +1,93 @@ +"""Certification Capability Hypotheses — capability-centric library + empirical confidence. + +Pins the reuse design (one capability, many supporting certs -> ~40-60 hypotheses, not ~300), the +automatic multi-certification merge, the empirical (computed) confidence loop, and the Welt-1 guarantee +that capabilities NO cert suggests (SBOM, signed updates, CVD) are never inferred -> they stay in the +delta and get asked. Then the Advisor consumes the resolved library end-to-end. +""" + +from __future__ import annotations + +import os + +import yaml + +from compliance.onboarding import ( + CapabilityHypothesis, + Observation, + ObservationType, + OnboardingInput, + advisor_start, + empirical_confidence, + empirical_distribution, + inferred_hypotheses, + resolve_for_certifications, +) +from compliance.transition_reasoning import TargetRequirement + +_DIR = os.path.dirname(__file__) +_LIB = [CapabilityHypothesis(**h) for h in yaml.safe_load( + open(os.path.join(_DIR, "..", "knowledge", "certification_hypotheses", "hypotheses.yaml"), encoding="utf-8"))["hypotheses"]] + + +def test_library_is_capability_centric_and_reuses_certs(): + # the shared core is small (reuse, not 30-per-cert) and document control is supported by many certs + doc = next(h for h in _LIB if h.capability == "document_and_change_control") + assert len(doc.supported_by) >= 4 + assert len(_LIB) <= 60 # whole library, not ~300 + + +def test_multi_certification_merges_automatically(): + # a company with ISO9001 + ISO14001 + TISAX gets the UNION of their hypotheses, deduped + merged = inferred_hypotheses(["ISO9001", "ISO14001", "TISAX"], _LIB) + caps = {h.capability for h in merged} + assert "document_and_change_control" in caps # ISO9001 + TISAX + assert "information_security_management" in caps # TISAX + assert "environmental_management_documentation" in caps # ISO14001 + # SBOM / signed updates are suggested by NO certificate -> never inferred + assert "sbom_creation" not in caps and "secure_signed_update_distribution" not in caps + + +def test_observations_are_richer_than_binary_and_review_gated(): + # the learning unit is the QUESTION; an answer can be partial with a scope note, not just yes/no + raw = [Observation(hypothesis_id="HYP-supplier", observation_type=ObservationType.CONFIRMED)] + assert empirical_confidence(raw) is None # unreviewed -> does NOT calibrate (review gate) + obs = [ + Observation(hypothesis_id="HYP-supplier", observation_type=ObservationType.CONFIRMED, reviewed=True), + Observation(hypothesis_id="HYP-supplier", observation_type=ObservationType.PARTIAL, + scope_note="nur kritische Lieferanten", reviewed=True), + Observation(hypothesis_id="HYP-supplier", observation_type=ObservationType.REFUTED, reviewed=True), + Observation(hypothesis_id="HYP-supplier", observation_type=ObservationType.NOT_APPLICABLE, reviewed=True), + ] + dist = empirical_distribution(obs) # a DISTRIBUTION, not a single percentage + assert dist["confirmed"] == 1 and dist["partial"] == 1 and dist["refuted"] == 1 and dist["not_applicable"] == 1 + # confidence = (confirmed + 0.5*partial) / (confirmed+partial+refuted); n.a. excluded from the base + assert empirical_confidence(obs) == 0.5 + + +def test_resolve_adapts_to_advisor_input(): + res = resolve_for_certifications(["ISO27001", "ISO9001"], _LIB) + assert "incident_management" in res["ISO27001"] + assert "document_and_change_control" in res["ISO9001"] + + +def test_iso13485_does_not_suggest_security_incident_management(): + # ISO 13485 CAPA / quality-safety incident handling is NOT security incident management -> too broad, + # removed from the incident_management hypothesis (review decision 2026-06-28). + res = resolve_for_certifications(["ISO13485"], _LIB) + assert "incident_management" not in res.get("ISO13485", []) + inc = next(h for h in _LIB if h.capability == "incident_management") + assert "ISO13485" not in inc.supported_by + + +def test_advisor_consumes_the_library_end_to_end(): + cra = yaml.safe_load(open(os.path.join(_DIR, "..", "knowledge", "transition_patterns", + "transition_pattern_iso27001_to_cra_maschinenvo_v1.yaml"), encoding="utf-8")) + req = [TargetRequirement(capability_id=a["capability"]) for a in cra["likely_covered"]] + req += [TargetRequirement(capability_id=d["capability"], expected_evidence=d.get("expected_evidence", [])) + for d in cra["delta_requirements"]] + inp = OnboardingInput(company="x", certifications=["ISO27001", "TISAX", "ISO9001", "ISO14001"], target=["CRA"]) + hyp = resolve_for_certifications(inp.certifications, _LIB) # library -> advisor input + res = advisor_start(inp, hyp, req, target_id="CRA", corpus_status={"CRA": "validated"}) + assert res.inferred_assumptions and res.next_best_questions + assert any(r.certification == "ISO14001" for r in res.rejected_assumptions) # not relevant to CRA diff --git a/backend-compliance/tests/test_completeness.py b/backend-compliance/tests/test_completeness.py new file mode 100644 index 00000000..530c19d6 --- /dev/null +++ b/backend-compliance/tests/test_completeness.py @@ -0,0 +1,97 @@ +"""Tests for the Regulatory Completeness Engine — auditable coverage, not confidence. + +Acceptance: separate identified from assessed regulations; justify every gap (corpus gap -> +future_corpus, applicability uncertain -> query_required with a deciding question); report counts +(never a single percentage); emit an honest audit statement. The product shows what it does NOT +know and why. +""" + +from __future__ import annotations + +from compliance.completeness import CompletenessReport, CorpusStatus, assess_completeness + +IDENTIFIED = ["CRA", "MaschinenVO", "EMV", "Environmental", "DataAct"] +CORPUS = {"CRA": "validated", "MaschinenVO": "validated", "EMV": "validated", + "Environmental": "unsupported", "DataAct": "validated"} +UNCERTAIN = [{"regulation": "DataAct", "deciding_question": "generates_usage_data", "reason": "generates_usage_data unbekannt"}] + + +def _report(): + return assess_completeness(IDENTIFIED, CORPUS, uncertain=UNCERTAIN, + assumptions=[{"key": "funkmodul", "value": "nein"}], assessed_obligations=128) + + +def test_assessed_excludes_uncertain_even_if_corpus_validated(): + r = _report() + # DataAct has a validated corpus but uncertain applicability -> NOT assessed + assert r.assessed_regulations == ["CRA", "EMV", "MaschinenVO"] + assert "DataAct" in r.open_regulations and "Environmental" in r.open_regulations + + +def test_corpus_gap_vs_applicability_exclusion(): + r = _report() + by = {e.subject: e for e in r.exclusions} + assert by["DataAct"].resolution == "query_required" and by["DataAct"].deciding_question == "generates_usage_data" + assert by["Environmental"].resolution == "future_corpus" + + +def test_open_corpora_is_unsupported_only(): + r = _report() + # DataAct corpus is validated (only applicability is open) -> NOT an open corpus + assert r.open_corpora == ["Environmental"] + + +def test_justification_present_when_every_gap_has_a_reason(): + r = _report() + assert r.justification_present is True + open_subjects = {e.subject for e in r.exclusions} + assert set(r.open_regulations) <= open_subjects + + +def test_counts_summary_has_no_percentage(): + r = _report() + assert "%" not in r.completeness_summary and "%" not in r.audit_statement + assert "Identifiziert 5" in r.completeness_summary and "bewertet 3" in r.completeness_summary + + +def test_audit_statement_is_honest(): + r = _report() + assert "3 von 5" in r.audit_statement and "bewusst nicht bewertet" in r.audit_statement + + +def test_draft_corpus_is_in_review_exclusion(): + r = assess_completeness(["CRA", "IEC62443"], {"CRA": "validated", "IEC62443": "draft"}) + by = {e.subject: e for e in r.exclusions} + assert by["IEC62443"].resolution == "in_review" + assert "IEC62443" in r.open_regulations and "IEC62443" not in r.open_corpora # draft != missing corpus + + +def test_all_assessed_no_open(): + r = assess_completeness(["CRA", "MaschinenVO"], {"CRA": "validated", "MaschinenVO": "validated"}) + assert r.open_regulations == [] and r.exclusions == [] + assert r.justification_present is True + assert "alle 2" in r.audit_statement + + +def test_coverage_status_mapped(): + r = _report() + cov = {c.regulation: c.status for c in r.coverage} + assert cov["CRA"] == CorpusStatus.VALIDATED and cov["Environmental"] == CorpusStatus.UNSUPPORTED + + +def test_assumptions_and_obligations_carried(): + r = _report() + assert r.assessed_obligations == 128 + assert [a.key for a in r.assumptions] == ["funkmodul"] + + +def test_unknown_regulation_defaults_to_open_corpus(): + r = assess_completeness(["CRA", "REACH"], {"CRA": "validated"}) # REACH not in registry -> unknown + assert "REACH" in r.open_corpora and r.assessed_regulations == ["CRA"] + + +def test_deterministic_and_type(): + r1 = _report() + r2 = _report() + assert r1.model_dump() == r2.model_dump() + assert isinstance(r1, CompletenessReport) diff --git a/backend-compliance/tests/test_customer_mission.py b/backend-compliance/tests/test_customer_mission.py new file mode 100644 index 00000000..0d16dcd9 --- /dev/null +++ b/backend-compliance/tests/test_customer_mission.py @@ -0,0 +1,50 @@ +"""End-to-end test for Customer Mission #1 — the whole platform as ONE connected expert system. + +This is NOT a knowledge-correctness test (that is the Reference Scenarios). It runs the FULL consulting +flow with the real engines and asserts the flow-continuity audit: the platform must carry a synthetic +machine builder from „what applies?" to a prioritised 6-month plan, with exactly the two known jumps +(Scope→Journey selector missing; Evidence→Verification parked) and no others creeping in. +""" + +from __future__ import annotations + +import os +import subprocess +import sys + + +def _run_mission(): + root = os.path.join(os.path.dirname(__file__), "..") + r = subprocess.run( + [sys.executable, "reference_scenarios/mission_machine_builder.py"], + cwd=root, env={**os.environ, "PYTHONPATH": "."}, capture_output=True, text=True, + ) + assert r.returncode == 0, r.stderr + return r.stdout + + +def test_mission_runs_end_to_end(): + out = _run_mission() + assert "Customer Mission #1" in out and "Flow-Continuity-Audit" in out + # the consulting answer must be produced (top-5 leverage closes 9/16 = 56%) + assert "56%" in out and "6-Monats-Antwort" in out + + +def test_exactly_two_real_jumps_no_regressions(): + out = _run_mission() + # the flow must stay continuous: exactly the two KNOWN seams, no new ones + assert "2 Sprünge" in out + assert "Scope → Journey" in out and "Evidence → Verification" in out + + +def test_full_consulting_flow_present(): + out = _run_mission() + for stage in ["1. Scope", "2. Journey", "3. Capability Delta", "4. Roadmap", + "5. Playbooks", "6. Nachweise", "7. Verification", "8. Completeness"]: + assert stage in out + + +def test_no_real_company_names(): + out = _run_mission().lower() + for name in ["eto", "owis", "winterhalter"]: + assert name not in out # synthetic archetype only diff --git a/backend-compliance/tests/test_customer_mission_2.py b/backend-compliance/tests/test_customer_mission_2.py new file mode 100644 index 00000000..39a79a21 --- /dev/null +++ b/backend-compliance/tests/test_customer_mission_2.py @@ -0,0 +1,70 @@ +"""Customer Mission #2 — the company arrives with a PROFILE, not a journey. + +Pins the reframe Mission #2 proves with the real engines: the start state is a Company Capability +Profile (many certs aggregated), certifications are observations/evidence, and more evidence shrinks +the delta (single-cert 12 → multi-cert 9). The „journey" is the computed delta `(Profile, Target)`, +not a thing a selector picks — which shrinks Mission #1's one open seam. +""" + +from __future__ import annotations + +import os +import subprocess +import sys + + +def _run_mission(): + root = os.path.join(os.path.dirname(__file__), "..") + r = subprocess.run( + [sys.executable, "reference_scenarios/mission_multicert_cra.py"], + cwd=root, env={**os.environ, "PYTHONPATH": "."}, capture_output=True, text=True, + ) + assert r.returncode == 0, r.stderr + return r.stdout + + +def test_mission_runs_end_to_end(): + out = _run_mission() + assert "Customer Mission #2" in out + assert "Company Capability Profile — der eigentliche Startzustand" in out + + +def test_more_evidence_shrinks_the_delta(): + out = _run_mission() + # multi-cert profile (9) must beat the single-cert counterfactual (12) — evidence is additive. + assert "**Delta dieses Profils:** 9" in out + assert "**Gegenprobe (nur ISO 27001):** 12" in out + assert "Mehr Evidence → kleineres Delta" in out + + +def test_reframe_no_per_certificate_journey(): + out = _run_mission() + # the journey is the computed delta (Profile, Target), not a selected cert→target transition. + assert "Keine per-Zertifikat-Journey" in out + assert "Company Capability Profile → CRA" in out + assert "Es wurde **keine Journey ausgewählt**" in out + + +def test_five_selection_rationale_questions_present(): + out = _run_mission() + for q in [ + "Welche Journey wurde gewählt?", + "Warum?", + "Welche Informationen waren für die Auswahl entscheidend?", + "Musste das Journey-Modell erweitert werden?", + "Musste ein neuer Selektionsparameter eingeführt werden?", + ]: + assert q in out + + +def test_evidence_is_target_relative(): + out = _run_mission() + # honest: ISO 14001 is in the profile but does not help the CRA; PSIRT covers two CRA-delta caps. + assert "ISO 14001" in out and "NICHT relevant" in out + assert "PSIRT" in out + + +def test_no_real_company_names(): + out = _run_mission().lower() + for name in ["eto", "owis", "winterhalter"]: + assert name not in out diff --git a/backend-compliance/tests/test_customer_mission_3.py b/backend-compliance/tests/test_customer_mission_3.py new file mode 100644 index 00000000..2ca22e4f --- /dev/null +++ b/backend-compliance/tests/test_customer_mission_3.py @@ -0,0 +1,58 @@ +"""Customer Mission #3 — one profile, three target TYPES (Requirements Verification proof). + +Pins what Mission #3 proves with the real engines: the pipeline is target-type-agnostic (a regulation, +a certification and a contract all reduce to required capabilities and run through assess_transition), +and Evidence-Relevance is target-relative (the same certification is worth a different amount against a +different target — PSIRT is hoch against the CRA, keine against TISAX, mittel against the tender). +""" + +from __future__ import annotations + +import os +import subprocess +import sys + + +def _run(): + root = os.path.join(os.path.dirname(__file__), "..") + r = subprocess.run( + [sys.executable, "reference_scenarios/mission_three_target_types.py"], + cwd=root, env={**os.environ, "PYTHONPATH": "."}, capture_output=True, text=True, + ) + assert r.returncode == 0, r.stderr + return r.stdout + + +def test_runs_end_to_end(): + out = _run() + assert "Customer Mission #3" in out + assert "ZIELTYP-AGNOSTISCH" in out + + +def test_three_target_types_one_engine(): + out = _run() + # a regulation, a certification and a contract all rendered through the same engine + for kind in ["Regulation", "Certification", "Contract"]: + assert kind in out + assert "Öffentliche Ausschreibung" in out + + +def test_evidence_relevance_is_target_relative(): + out = _run() + # the headline demonstration: PSIRT ranks differently per target + assert "**PSIRT** | hoch (3) | keine (0) | mittel (1) |" in out + # ISO 14001 is irrelevant to all three security/quality targets (but noted hoch for environmental) + assert "**ISO14001** | keine (0) | keine (0) | keine (0) |" in out + assert "Relevanz ist eine Funktion des Ziels" in out + + +def test_cross_target_type_convergence(): + out = _run() + assert "über Zieltypen hinweg" in out + assert "Zielarten gleichzeitig" in out + + +def test_no_real_company_names(): + out = _run().lower() + for name in ["eto", "owis", "winterhalter"]: + assert name not in out diff --git a/backend-compliance/tests/test_customer_mission_4.py b/backend-compliance/tests/test_customer_mission_4.py new file mode 100644 index 00000000..dd3881a8 --- /dev/null +++ b/backend-compliance/tests/test_customer_mission_4.py @@ -0,0 +1,57 @@ +"""Customer Mission #4 — a second, different contract target (no tender-special-logic). + +Pins what Mission #4 guards: TWO structurally different contract sub-types (a public tender and a +private OEM Lastenheft) run through the identical engine and produce DIFFERENT, non-overlapping deltas +with no per-contract code. That is the evidence that the later Scope→Journey selector can treat any +contract as a plain Required set — no tender-shaped special case baked in. +""" + +from __future__ import annotations + +import os +import subprocess +import sys + + +def _run(): + root = os.path.join(os.path.dirname(__file__), "..") + r = subprocess.run( + [sys.executable, "reference_scenarios/mission_second_contract.py"], + cwd=root, env={**os.environ, "PYTHONPATH": "."}, capture_output=True, text=True, + ) + assert r.returncode == 0, r.stderr + return r.stdout + + +def test_runs_end_to_end(): + out = _run() + assert "Customer Mission #4" in out + assert "kein Contract-Spezialfall" in out + + +def test_two_distinct_contract_types_one_engine(): + out = _run() + assert "public tender" in out and "private OEM spec" in out + assert "keinen Contract-spezifischen Codepfad" in out + + +def test_contracts_produce_different_deltas(): + out = _run() + # the two contracts must be genuinely different: their deltas do not overlap + assert "**Beiden gemeinsam:** —" in out + # each carries its own distinctive missing capabilities + assert "penetration_test_evidence" in out # tender-only + assert "functional_safety_evidence" in out # OEM-only + + +def test_evidence_relevance_differs_between_contracts(): + out = _run() + # TISAX is worth more against the automotive OEM spec than the generic tender + assert "**TISAX** | hoch (4) | hoch (6) |" in out + assert "Relevanz ist eine Funktion des Ziels" in out + + +def test_no_real_company_names(): + out = _run().lower() + for name in ["eto", "owis", "winterhalter"]: + assert name not in out diff --git a/backend-compliance/tests/test_customer_mission_5.py b/backend-compliance/tests/test_customer_mission_5.py new file mode 100644 index 00000000..6901e288 --- /dev/null +++ b/backend-compliance/tests/test_customer_mission_5.py @@ -0,0 +1,55 @@ +"""Customer Mission #5 — a non-security target: evidence relevance flips both ways. + +Pins the one claim this mission exists to prove: relevance(evidence, target) is a function of the +TARGET, not an attribute of the evidence. The same ISO 14001 is keine against CRA/TISAX but hoch +against an environmental/material target, while the security certs flip the other way (hoch against +security targets, keine against the environmental one). Tight scope: no corpus, no norm model, no +new runtime module, no real names. +""" + +from __future__ import annotations + +import os +import subprocess +import sys + + +def _run(): + root = os.path.join(os.path.dirname(__file__), "..") + r = subprocess.run( + [sys.executable, "reference_scenarios/mission_non_security_target.py"], + cwd=root, env={**os.environ, "PYTHONPATH": "."}, capture_output=True, text=True, + ) + assert r.returncode == 0, r.stderr + return r.stdout + + +def test_runs_end_to_end(): + out = _run() + assert "Customer Mission #5" in out + + +def test_iso14001_relevance_flips_to_high_on_environmental_target(): + out = _run() + # the headline: same cert, keine against security targets, hoch against the environmental one + assert "**ISO14001** | keine (0) | keine (0) | hoch (3) |" in out + + +def test_security_evidence_is_worthless_against_environmental_target(): + out = _run() + # symmetry: security certs are relevant for security, keine for the environmental target + assert "**ISO27001** | hoch (5) | hoch (3) | keine (0) |" in out + assert "**PSIRT** | hoch (3) | keine (0) | keine (0) |" in out + + +def test_relevance_is_a_function_of_the_target(): + out = _run() + assert "relevance(evidence, target)` zweiseitig bewiesen" in out + # five target types now covered -> selector becomes sensible + assert "fünf Zielarten" in out + + +def test_no_real_company_names(): + out = _run().lower() + for name in ["eto", "owis", "winterhalter"]: + assert name not in out diff --git a/backend-compliance/tests/test_domain_programs.py b/backend-compliance/tests/test_domain_programs.py new file mode 100644 index 00000000..8880ad3f --- /dev/null +++ b/backend-compliance/tests/test_domain_programs.py @@ -0,0 +1,101 @@ +"""Characterization tests for the Domain Knowledge Program v1 backlog (data, not code). + +Pins the program FRAMEWORK contract: a ranked backlog of domain definitions, each entered by INDUSTRY +with its typical requirement sources + a pre-onboarding capability hypothesis (typical_certifications). +Industrial Automation is rank 1. Environmental stays law-first. If a future edit reorders the backlog, +drops a source list, or reverts environmental to an ISO-first framing, these tests fail. +""" + +from __future__ import annotations + +import os + +import yaml + +_DIR = os.path.join(os.path.dirname(__file__), "..", "knowledge", "programs") + + +def _programs(): + out = {} + for f in sorted(os.listdir(_DIR)): + if f.endswith(".yaml"): + with open(os.path.join(_DIR, f), encoding="utf-8") as h: + p = yaml.safe_load(h) + if "backlog_rank" in p: # domain programs only (not transitions backlog) + out[p["id"]] = p + return out + + +def _transitions(): + with open(os.path.join(_DIR, "transitions.yaml"), encoding="utf-8") as h: + return yaml.safe_load(h) + + +def test_five_domains_ranked_backlog(): + ranks = sorted(p["backlog_rank"] for p in _programs().values()) + assert ranks == [1, 2, 3, 4, 5] + + +def test_industrial_automation_is_rank_1(): + progs = _programs() + rank1 = [p for p in progs.values() if p["backlog_rank"] == 1] + assert len(rank1) == 1 and rank1[0]["id"] == "PROG-industrial-automation" + assert {"CRA", "MaschinenVO"} <= set(rank1[0]["typical_requirement_sources"]) + + +def test_every_domain_entered_by_industry_with_sources_and_hypothesis(): + for p in _programs().values(): + assert p.get("industry") and p.get("customer_entry") # industry-first entry + assert p["typical_requirement_sources"] # stage 2 defined + assert p["typical_certifications"] # pre-onboarding capability hypothesis (ETO) + + +def test_no_stored_stage_status_progress_is_derived(): + # the 7-stage progress is computed-not-stored: program shells must NOT hard-code stage status + for p in _programs().values(): + assert "stages" not in p + + +def test_environmental_stays_law_first(): + env = _programs()["PROG-environmental"] + assert "ISO 14001 ist KEIN Umweltrecht" in env["principle"] + assert set(env["typical_requirement_sources"]) == {"water", "chemicals", "emissions", "energy", "waste", "product_responsibility"} + + +def test_automotive_and_medical_present(): + progs = _programs() + assert "TISAX" in progs["PROG-automotive"]["typical_requirement_sources"] + assert "MDR" in progs["PROG-medical"]["typical_requirement_sources"] + + +def test_readme_documents_seven_stage_checklist(): + with open(os.path.join(_DIR, "README.md"), encoding="utf-8") as h: + readme = h.read() + for stage in ["Domain Model", "Requirement Sources", "Capability Registry", + "Transition Patterns", "Playbooks", "Reference Scenarios", "Completeness"]: + assert stage in readme + assert "Industrial Automation" in readme # backlog #1 documented + + +def test_transition_backlog_has_top_demanded_transitions(): + ts = _transitions()["transitions"] + pairs = {(t["from"], t["to"]) for t in ts} + # the highest-value transitions customers actually buy + assert ("ISO27001", "CRA") in pairs and ("ISO9001", "CRA") in pairs + assert ("ISO9001", "MaschinenVO") in pairs + + +def test_transition_backlog_is_prioritised(): + ts = _transitions()["transitions"] + for t in ts: + assert 1 <= t["priority"] <= 5 and t["from"] and t["to"] + # the five-star transitions are the CRA/MaschinenVO entry points + five = {(t["from"], t["to"]) for t in ts if t["priority"] == 5} + assert ("ISO9001", "MaschinenVO") in five + + +def test_transition_is_the_unit_documented(): + with open(os.path.join(_DIR, "README.md"), encoding="utf-8") as h: + readme = h.read() + assert "unit of knowledge is the TRANSITION" in readme + assert "Operational Knowledge" in readme and "Verification Knowledge" in readme # three layers diff --git a/backend-compliance/tests/test_environmental_stress_test.py b/backend-compliance/tests/test_environmental_stress_test.py new file mode 100644 index 00000000..af15ad3f --- /dev/null +++ b/backend-compliance/tests/test_environmental_stress_test.py @@ -0,0 +1,64 @@ +"""Environmental stress test — does the architecture work OUTSIDE cyber? (Phase Ω) + +Pins the first NON-cyber generality proof: ISO 14001 (an EMS, as a Company Profile) runs through the +SAME RS-005 engine + Journey Matcher used for ISO 27001 -> CRA, with only new DATA (a pattern YAML + +injected Required caps) and zero runtime code. ISO 14001 yields environmental MANAGEMENT capabilities +(Welt-1); the concrete substance/emission/water/material evidence is the delta; rejected_assumptions +state what ISO 14001 does NOT produce; and the Journey Matcher stays domain-agnostic (cyber journeys 0%). +""" + +from __future__ import annotations + +import os +import subprocess +import sys + + +def _run(): + root = os.path.join(os.path.dirname(__file__), "..") + r = subprocess.run( + [sys.executable, "reference_scenarios/environmental_stress_test.py"], + cwd=root, env={**os.environ, "PYTHONPATH": "."}, capture_output=True, text=True, + ) + assert r.returncode == 0, r.stderr + return r.stdout + + +def test_runs_end_to_end_outside_cyber(): + out = _run() + assert "AUSSERHALB von Cyber" in out + assert "keine Zeile neuer Runtime-Code" in out + + +def test_iso14001_is_management_not_evidence(): + out = _run() + # 5 management capabilities probably present, 11 concrete-evidence capabilities missing + assert "5 vermutlich vorhanden, 11 fehlt" in out + assert "manage_chemical_substances" in out # a verb capability + + +def test_rejected_assumptions_preserve_welt1_welt2(): + out = _run() + assert "rejected_assumptions" in out + assert "ISO 14001 does NOT produce concrete substance lists or REACH registrations." in out + assert "Welt-1/Welt-2-Trennung bleibt erhalten" in out + + +def test_journey_matcher_stays_domain_agnostic(): + out = _run() + # the environmental journey explains the delta; cyber journeys explain 0% + assert "| ISO14001 -> Environmental | 100% |" in out + assert "| ISMS -> TISAX | 0% |" in out + assert "| ISO27001 -> CRA + MaschinenVO | 0% |" in out + + +def test_zero_runtime_change_verdict(): + out = _run() + assert "0 neue Runtime-Klassen, 0 neue Pipeline" in out + assert "16 neue Capability-Typen" in out + + +def test_no_real_company_names(): + out = _run().lower() + for name in ["eto", "owis", "winterhalter"]: + assert name not in out diff --git a/backend-compliance/tests/test_journey_matcher.py b/backend-compliance/tests/test_journey_matcher.py new file mode 100644 index 00000000..1b7c7504 --- /dev/null +++ b/backend-compliance/tests/test_journey_matcher.py @@ -0,0 +1,80 @@ +"""Unit tests for the Journey Matcher (Delta -> Journey). + +The matcher ranks known journeys by the share of the Capability Delta they EXPLAIN, using ONLY the +delta and injected capability-cluster signatures — deterministic, auditable, no ML. These tests pin +the score semantics (recall over the delta), the ranking order, the audit reasons, and that context +corroborates without ever changing the score. +""" + +from __future__ import annotations + +from compliance.journey_matcher import ( + JourneySignature, + MatchContext, + match_journeys, +) + + +def _sig(jid, pattern, **kw): + return JourneySignature(journey_id=jid, label=jid, capability_pattern=pattern, **kw) + + +def test_score_is_share_of_delta_explained(): + delta = ["a", "b", "c", "d", "e"] + j = _sig("J", ["a", "b", "c", "d"]) # explains 4 of 5 + res = match_journeys(delta, [j]) + assert res.matches[0].score == 0.8 + assert res.matches[0].explains == "4 von 5 fehlenden Capabilities" + + +def test_ranking_orders_by_explanatory_power(): + delta = ["a", "b", "c", "d"] + journeys = [ + _sig("low", ["a"]), # 1/4 + _sig("high", ["a", "b", "c"]), # 3/4 + _sig("mid", ["a", "b"]), # 2/4 + ] + res = match_journeys(delta, journeys) + assert [m.journey_id for m in res.matches] == ["high", "mid", "low"] + assert res.best.journey_id == "high" + + +def test_audit_reason_partitions_the_delta(): + delta = ["a", "b", "c"] + j = _sig("J", ["b", "c", "x", "y"]) # explains b,c; misses a; reaches beyond into x,y + r = match_journeys(delta, [j]).matches[0].reason + assert r.matched_capabilities == ["b", "c"] + assert r.unexplained_delta == ["a"] + assert r.journey_only == ["x", "y"] + + +def test_context_corroborates_but_never_changes_score(): + delta = ["a", "b"] + same = _sig("same", ["a", "b"], target_type="regulation") + other = _sig("other", ["a", "b"], target_type="contract") + ctx = MatchContext(target_type="regulation") + res = match_journeys(delta, [other, same], ctx) + # identical score (1.0) -> tie broken by context-signal count: 'same' first + assert res.matches[0].score == res.matches[1].score == 1.0 + assert res.matches[0].journey_id == "same" + assert "gleiche Zielart" in res.matches[0].reason.context_signals + assert res.matches[1].reason.context_signals == [] + + +def test_deterministic_tiebreak_by_journey_id(): + delta = ["a", "b"] + res = match_journeys(delta, [_sig("zeta", ["a"]), _sig("alpha", ["a"])]) + assert [m.journey_id for m in res.matches] == ["alpha", "zeta"] + + +def test_no_journey_explains_the_delta(): + res = match_journeys(["a", "b"], [_sig("J", ["x", "y"])]) + assert res.best is None + assert res.matches[0].score == 0.0 + assert "neue Journey-Kandidatin" in res.headline + + +def test_empty_delta_yields_no_best(): + res = match_journeys([], [_sig("J", ["a"])]) + assert res.delta_size == 0 + assert res.best is None diff --git a/backend-compliance/tests/test_journey_matcher_demo.py b/backend-compliance/tests/test_journey_matcher_demo.py new file mode 100644 index 00000000..2792baa7 --- /dev/null +++ b/backend-compliance/tests/test_journey_matcher_demo.py @@ -0,0 +1,51 @@ +"""Journey Matcher demo test — Delta -> Journey on the real transition patterns. + +Pins that the matcher, given ONLY a real Capability Delta (a multi-cert company wanting CRA + +MaschinenVO), correctly ranks the known journeys by explanatory power: the convergence journey +explains the whole delta, the CRA-only journey explains the security part but misses the machine- +safety capabilities, and the TISAX journey is irrelevant. End-to-end through the real engines. +""" + +from __future__ import annotations + +import os +import subprocess +import sys + + +def _run(): + root = os.path.join(os.path.dirname(__file__), "..") + r = subprocess.run( + [sys.executable, "reference_scenarios/journey_matcher_demo.py"], + cwd=root, env={**os.environ, "PYTHONPATH": "."}, capture_output=True, text=True, + ) + assert r.returncode == 0, r.stderr + return r.stdout + + +def test_runs_end_to_end(): + out = _run() + assert "Journey Matcher" in out + assert "Goal → Required → Delta → Journey" in out + + +def test_convergence_journey_explains_the_whole_delta(): + out = _run() + assert "**ISO27001 -> CRA + MaschinenVO** | 9 von 9 fehlenden Capabilities | 100% |" in out + + +def test_partial_journey_misses_machine_safety(): + out = _run() + # CRA-only journey explains the security part but not the MaschinenVO capabilities + assert "**ISO27001 -> CRA** | 5 von 9 fehlenden Capabilities | 56% |" in out + + +def test_irrelevant_journey_scores_zero(): + out = _run() + assert "**ISMS -> TISAX** | 0 von 9 fehlenden Capabilities | 0% |" in out + + +def test_match_is_auditable(): + out = _run() + assert "auditierbar, keine Blackbox" in out + assert "Erklärte Capabilities" in out diff --git a/backend-compliance/tests/test_knowledge_intake.py b/backend-compliance/tests/test_knowledge_intake.py new file mode 100644 index 00000000..06dbcf1d --- /dev/null +++ b/backend-compliance/tests/test_knowledge_intake.py @@ -0,0 +1,97 @@ +"""Tests for Knowledge Intake — classify a document and assess its impact (no extraction, no LLM). + +Acceptance: build a deterministic index from existing knowledge; for an incoming document, surface +which capabilities / playbooks / patterns / reference scenarios / obligations it probably touches, +whether it is a new domain, and triage it (HIGH / LOW / NONE / NEW_DOMAIN). The point: of N documents, +which few actually change our knowledge. +""" + +from __future__ import annotations + +from compliance.knowledge_intake import ( + DocumentDescriptor, ImpactLevel, KnowledgeIndex, + assess_document_impact, build_knowledge_index, +) + +PATTERNS = [ + {"id": "TP-A", "transition_goal": {"to": {"regulation": "CRA"}}, + "delta_requirements": [{"capability": "sbom_creation", "covers_targets": ["CRA"]}, + {"capability": "coordinated_vulnerability_disclosure", "covers_targets": ["CRA"]}]}, + {"id": "TP-B", "transition_goal": {"to": [{"regulation": "CRA"}, {"regulation": "MaschinenVO"}]}, + "delta_requirements": [{"capability": "machine_guards", "covers_targets": ["MaschinenVO"]}]}, +] +PLAYBOOKS = [{"capability_id": "sbom_creation"}] +RTS = [{"id": "RTS-1", "transition_goal": {"to": [{"target": "CRA"}]}}] + + +def _index(): + return build_knowledge_index(PATTERNS, PLAYBOOKS, RTS, obligation_index={"CRA": ["o1", "o2"]}) + + +def test_build_index_extracts_regs_caps_playbooks(): + idx = _index() + assert "CRA" in idx.regulations and "MaschinenVO" in idx.regulations + assert idx.capability_regulations["sbom_creation"] == ["CRA"] + assert idx.playbook_capabilities == ["sbom_creation"] + assert idx.transition_patterns["TP-B"] == ["CRA", "MaschinenVO"] # list-form to[] handled + assert idx.reference_scenarios["RTS-1"] == ["CRA"] # target key handled + + +def test_affected_by_regulation(): + kp = assess_document_impact(DocumentDescriptor(document_id="d", regulations=["CRA"]), _index()) + assert "sbom_creation" in kp.affected_capabilities + assert "TP-A" in kp.affected_transition_patterns and "RTS-1" in kp.affected_reference_scenarios + assert kp.affected_obligations == ["o1", "o2"] + assert not kp.new_domain + + +def test_affected_by_keyword_even_without_regulation(): + # a doc with no declared regulation but keyword 'sbom' still finds the capability + kp = assess_document_impact(DocumentDescriptor(document_id="d", keywords=["sbom"]), _index()) + assert "sbom_creation" in kp.affected_capabilities + assert "sbom_creation" in kp.affected_playbooks + + +def test_playbooks_are_affected_caps_with_a_playbook(): + kp = assess_document_impact(DocumentDescriptor(document_id="d", regulations=["CRA"]), _index()) + assert kp.affected_playbooks == ["sbom_creation"] # cvd has no playbook here + + +def test_new_domain_when_only_unknown_regulations(): + kp = assess_document_impact(DocumentDescriptor(document_id="d", regulations=["UmweltVO"]), _index()) + assert kp.new_domain and kp.impact_level == ImpactLevel.NEW_DOMAIN + assert kp.unknown_regulations == ["UmweltVO"] + assert kp.affected_capabilities == [] + + +def test_none_when_nothing_matches(): + kp = assess_document_impact(DocumentDescriptor(document_id="d", keywords=["newsletter"]), _index()) + assert kp.impact_level == ImpactLevel.NONE and not kp.new_domain + assert "ignorierbar" in kp.recommendation + + +def test_high_impact_triage(): + kp = assess_document_impact(DocumentDescriptor(document_id="d", regulations=["CRA"]), _index()) + # >= 3 affected caps OR a playbook -> HIGH + assert kp.impact_level == ImpactLevel.HIGH + assert "priorisieren" in kp.recommendation + + +def test_low_impact_when_small_and_no_playbook(): + idx = KnowledgeIndex(regulations=["CRA"], capability_regulations={"x": ["CRA"]}, playbook_capabilities=[]) + kp = assess_document_impact(DocumentDescriptor(document_id="d", regulations=["CRA"]), idx) + assert kp.impact_level == ImpactLevel.LOW and kp.affected_capabilities == ["x"] + + +def test_classification_echoed(): + kp = assess_document_impact(DocumentDescriptor(document_id="d", regulations=["CRA"], keywords=["SBOM"], document_type="faq"), _index()) + assert kp.classification["regulations"] == ["CRA"] + assert kp.classification["keywords"] == ["sbom"] and kp.classification["document_type"] == ["faq"] + + +def test_deterministic(): + idx = _index() + d = DocumentDescriptor(document_id="d", regulations=["CRA"], keywords=["sbom"]) + a = assess_document_impact(d, idx) + b = assess_document_impact(d, idx) + assert a.model_dump() == b.model_dump() diff --git a/backend-compliance/tests/test_knowledge_production.py b/backend-compliance/tests/test_knowledge_production.py new file mode 100644 index 00000000..283bb1a0 --- /dev/null +++ b/backend-compliance/tests/test_knowledge_production.py @@ -0,0 +1,89 @@ +"""Tests for Knowledge Production — the Playbook Draft Generator. + +Acceptance: deterministically assemble a playbook DRAFT for a capability from a transition-pattern +delta requirement (why / closes / evidence with provenance), leaving practitioner know-how as an +explicit TODO; turn a whole pattern into one draft per delta capability. No LLM, fully deterministic. +The expert reviews drafts instead of writing from a blank page. +""" + +from __future__ import annotations + +from compliance.knowledge_production import ( + DraftStatus, PlaybookDraft, drafts_from_pattern, generate_playbook_draft, +) + +REQ = { + "capability": "sbom_creation", + "why_asked": "CRA requires an SBOM; MaschinenVO does not.", + "covers_targets": ["CRA"], + "expected_evidence": ["sbom"], +} +CONV_REQ = { + "capability": "product_cyber_risk_assessment", + "why_asked": "Both require assessing cyber threats.", + "covers_targets": ["CRA", "MaschinenVO"], + "expected_evidence": ["product_risk_assessment"], +} + + +def test_assembles_owned_fields_with_provenance(): + d = generate_playbook_draft("sbom_creation", REQ, control_links=["component_inventory"]) + assert d.status == DraftStatus.DRAFT_GENERATED + assert d.why.startswith("CRA requires an SBOM") + assert d.closes_regulations == ["CRA"] and d.expected_evidence == ["sbom"] + assert d.typical_controls == ["component_inventory"] + assert d.provenance["why"] == "transition_pattern:why_asked" + assert d.provenance["closes_regulations"] == "leverage:covers_targets" + assert d.provenance["typical_controls"] == "execution:control_links" + + +def test_soft_fields_are_todo(): + d = generate_playbook_draft("sbom_creation", REQ) + assert d.todo == ["tools", "process_steps", "how_others_do_it"] # practitioner know-how owed + + +def test_missing_owned_fields_go_to_todo(): + d = generate_playbook_draft("x", {}) + assert "why" in d.todo and "expected_evidence" in d.todo + assert d.closes_regulations == [] and d.typical_controls == [] + assert d.status == DraftStatus.DRAFT_GENERATED + + +def test_missing_because_fallback_for_why(): + d = generate_playbook_draft("x", {"missing_because": "no analogue in ISO 27001"}) + assert d.why == "no analogue in ISO 27001" and "why" not in d.todo + + +def test_closes_deduped_sorted_and_title_humanised(): + d = generate_playbook_draft("secure_signed_update_distribution", {"covers_targets": ["MaschinenVO", "CRA", "CRA"]}) + assert d.closes_regulations == ["CRA", "MaschinenVO"] + assert d.title == "secure signed update distribution" + + +def test_controls_default_empty_no_execution_data(): + d = generate_playbook_draft("x", REQ) + assert d.typical_controls == [] # nothing injected -> empty + + +def test_drafts_from_pattern_one_per_delta_in_order(): + pattern = {"delta_requirements": [REQ, CONV_REQ]} + drafts = drafts_from_pattern(pattern) + assert [d.capability_id for d in drafts] == ["sbom_creation", "product_cyber_risk_assessment"] + assert drafts[1].closes_regulations == ["CRA", "MaschinenVO"] # leverage 2 carried through + + +def test_drafts_from_pattern_injects_controls_and_skips_unnamed(): + pattern = {"delta_requirements": [REQ, {"why_asked": "no capability key"}]} + drafts = drafts_from_pattern(pattern, control_links_by_cap={"sbom_creation": ["c1"]}) + assert len(drafts) == 1 and drafts[0].typical_controls == ["c1"] # entry without capability skipped + + +def test_deterministic(): + pattern = {"delta_requirements": [REQ, CONV_REQ]} + a = [(d.capability_id, d.why, tuple(d.todo)) for d in drafts_from_pattern(pattern)] + b = [(d.capability_id, d.why, tuple(d.todo)) for d in drafts_from_pattern(pattern)] + assert a == b + + +def test_returns_playbookdraft_type(): + assert isinstance(generate_playbook_draft("x", REQ), PlaybookDraft) diff --git a/backend-compliance/tests/test_mcap_convergence_analysis.py b/backend-compliance/tests/test_mcap_convergence_analysis.py new file mode 100644 index 00000000..b9b18a44 --- /dev/null +++ b/backend-compliance/tests/test_mcap_convergence_analysis.py @@ -0,0 +1,78 @@ +"""Cross-Domain MCAP Convergence Analysis — impact over frequency (Phase Ω pause). + +Pins the deterministic MCAP Impact analysis and, critically, the anti-frequency-deception property: +a capability that bridges many target TYPES / domains / journeys (secure_signed_update_distribution) +must outrank a high-frequency single-domain management cap (conduct_internal_environmental_audits). +Four reports (Core / Emerging / Isolated / Suspicious), pure aggregation over existing data, no runtime. +""" + +from __future__ import annotations + +import os +import subprocess +import sys + + +def _run(): + root = os.path.join(os.path.dirname(__file__), "..") + r = subprocess.run( + [sys.executable, "reference_scenarios/mcap_convergence_analysis.py"], + cwd=root, env={**os.environ, "PYTHONPATH": "."}, capture_output=True, text=True, + ) + assert r.returncode == 0, r.stderr + return r.stdout + + +def _section(out, header): + start = out.index(header) + nxt = out.find("\n## ", start + 1) + return out[start: nxt if nxt != -1 else len(out)] + + +def test_runs_end_to_end(): + out = _run() + assert "Cross-Domain MCAP Convergence Analysis" in out + assert "Impact = distinct Sources" in out + + +def test_core_is_cross_cutting_not_frequency(): + out = _run() + core = _section(out, "## 1. Core MCAPs") + # the most cross-cutting capability tops the Core report; with Medical it now spans 3 domains + assert "`secure_signed_update_distribution` | **24** |" in core + assert "technical_vulnerability_management" in core + # a high-frequency BUT single-domain management cap must NOT be in Core (frequency != impact) + assert "conduct_internal_environmental_audits" not in core + + +def test_all_five_reports_present(): + out = _run() + for header in ["## 1. Core MCAPs", "## 2. Emerging MCAPs", "## 3. Isolated MCAPs", + "## 4. Suspicious MCAPs", "## 5. Missing Convergence"]: + assert header in out + + +def test_missing_convergence_flags_cross_source_duplication(): + out = _run() + mc = _section(out, "## 5. Missing Convergence") + # the 'risk' token clusters several distinct risk MCAPs across many sources -> a review candidate + assert "Token `risk`" in mc + assert "maintain_risk_management_file_iso14971" in mc and "product_cyber_risk_assessment" in mc + assert "KEIN Auto-Merge" in out + # single-source decompositions (all-environmental) must be filtered out (>=2 sources required) + assert "Token `environmental`" not in mc + + +def test_isolated_and_suspicious_are_review_tools(): + out = _run() + iso = _section(out, "## 3. Isolated MCAPs") + assert "Review: spezialisiert ODER Konvergenz übersehen" in iso + assert "conduct_clinical_evaluation" in iso or "cybersecurity_management_system" in iso + susp = _section(out, "## 4. Suspicious MCAPs") + assert "zu grob" in susp and "zu fein" in susp + + +def test_abstraction_level_signal(): + out = _run() + assert "richtigen Abstraktionsniveau" in out + assert "Strukturkern" in out \ No newline at end of file diff --git a/backend-compliance/tests/test_medical_stress_test.py b/backend-compliance/tests/test_medical_stress_test.py new file mode 100644 index 00000000..3d06caa3 --- /dev/null +++ b/backend-compliance/tests/test_medical_stress_test.py @@ -0,0 +1,58 @@ +"""Medical stress test — safety + security coupled (Phase Ω #3). + +Pins the harder joint test: ISO 13485 runs through the SAME engine (0 runtime, data only), and IEC +81001-5-1 (health-software security) pulls in the SAME security MCAPs as the CRA — so Medical REUSES +cyber capabilities (the safety/security coupling shows up as capability reuse) while adding genuinely +new medical-specific caps (clinical evaluation, software safety classification, ISO 14971 risk file). +""" + +from __future__ import annotations + +import os +import subprocess +import sys + + +def _run(): + root = os.path.join(os.path.dirname(__file__), "..") + r = subprocess.run( + [sys.executable, "reference_scenarios/medical_stress_test.py"], + cwd=root, env={**os.environ, "PYTHONPATH": "."}, capture_output=True, text=True, + ) + assert r.returncode == 0, r.stderr + return r.stdout + + +def test_runs_end_to_end_zero_runtime(): + out = _run() + assert "Medical Stress Test" in out + assert "0 neue Runtime-Klassen" in out + + +def test_safety_security_coupling_reuses_cyber_caps(): + out = _run() + assert "Wiederverwendete Cyber-Capabilities (4)" in out + for cap in ["secure_signed_update_distribution", "technical_vulnerability_management", + "access_control_and_authentication", "sbom_creation"]: + assert cap in out + assert "IEC 81001-5-1" in out + + +def test_genuinely_new_medical_capabilities(): + out = _run() + assert "Neu (7)" in out + for cap in ["conduct_clinical_evaluation", "classify_software_safety_iec62304", + "maintain_risk_management_file_iso14971", "perform_benefit_risk_analysis"]: + assert cap in out + + +def test_rejected_assumptions_present(): + out = _run() + assert "ISO 13485 does NOT produce clinical evidence" in out + assert "the same security caps as the CRA" in out + + +def test_no_real_company_names(): + out = _run().lower() + for name in ["eto", "owis", "winterhalter", "medtronic", "siemens"]: + assert name not in out diff --git a/backend-compliance/tests/test_observation_log.py b/backend-compliance/tests/test_observation_log.py new file mode 100644 index 00000000..4dc3e71b --- /dev/null +++ b/backend-compliance/tests/test_observation_log.py @@ -0,0 +1,73 @@ +"""Observation Log — append-only JSONL store + computed statistics (Task 59b/c v1). + +Pins the user's decision (2026-06-28): observations are CALIBRATION data, not product data -> an +append-only JSONL log under knowledge/observations/, NO DB, NO migration. Distribution and confidence are +COMPUTED from the log; only REVIEWED observations calibrate (review gate); a later review is a new line +that supersedes by observation_id. Nothing is ever written back to a hypothesis. +""" + +from __future__ import annotations + +from compliance.onboarding import ( + ObservationRecord, + ObservationType, + aggregate_by_hypothesis, + append_observation, + load_observations, + review_queue, +) + + +def _rec(oid, hyp, otype, reviewed=False, **kw): + return ObservationRecord( + observation_id=oid, hypothesis_id=hyp, observation_type=otype, reviewed=reviewed, + timestamp="2026-07-01T00:00:00Z", customer_archetype="machine_builder+ISO27001", **kw) + + +def test_append_only_round_trip(tmp_path): + p = str(tmp_path / "obs.jsonl") + append_observation(_rec("o1", "HYP-secure_dev", ObservationType.CONFIRMED, reviewed=True), p) + append_observation(_rec("o2", "HYP-secure_dev", ObservationType.REFUTED, reviewed=True), p) + recs = load_observations(p) + assert {r.observation_id for r in recs} == {"o1", "o2"} + assert all(r.customer_archetype == "machine_builder+ISO27001" for r in recs) # anonymised archetype, not a name + + +def test_review_supersedes_by_id_append_only(tmp_path): + p = str(tmp_path / "obs.jsonl") + append_observation(_rec("o1", "HYP-x", ObservationType.CONFIRMED, reviewed=False), p) # raw answer + append_observation(_rec("o1", "HYP-x", ObservationType.CONFIRMED, reviewed=True, + reviewed_by="anna"), p) # later review event + assert len(load_observations(p, reconcile=False)) == 2 # both lines kept (append-only) + recs = load_observations(p) # reconciled + assert len(recs) == 1 and recs[0].reviewed and recs[0].reviewed_by == "anna" + + +def test_statistics_apply_the_review_gate(tmp_path): + p = str(tmp_path / "obs.jsonl") + append_observation(_rec("a", "HYP-sdl", ObservationType.CONFIRMED, reviewed=True), p) + append_observation(_rec("b", "HYP-sdl", ObservationType.CONFIRMED, reviewed=True), p) + append_observation(_rec("c", "HYP-sdl", ObservationType.REFUTED, reviewed=True), p) + append_observation(_rec("d", "HYP-sdl", ObservationType.CONFIRMED, reviewed=False), p) # unreviewed -> ignored + stats = {s.hypothesis_id: s for s in aggregate_by_hypothesis(load_observations(p))} + s = stats["HYP-sdl"] + assert s.total_count == 4 and s.reviewed_count == 3 + assert s.distribution["confirmed"] == 2 and s.distribution["refuted"] == 1 # unreviewed one excluded + assert s.confidence == round(2 / 3, 2) # (2 + 0.5*0) / 3 + + +def test_review_queue_lists_unreviewed(tmp_path): + p = str(tmp_path / "obs.jsonl") + append_observation(_rec("a", "HYP-y", ObservationType.CONFIRMED, reviewed=True), p) + append_observation(_rec("b", "HYP-y", ObservationType.PARTIAL, reviewed=False), p) + q = review_queue(load_observations(p)) + assert [r.observation_id for r in q] == ["b"] + + +def test_load_directory_of_monthly_files(tmp_path): + d = tmp_path / "observations" + d.mkdir() + append_observation(_rec("a", "HYP-z", ObservationType.CONFIRMED, reviewed=True), str(d / "2026-06.jsonl")) + append_observation(_rec("b", "HYP-z", ObservationType.REFUTED, reviewed=True), str(d / "2026-07.jsonl")) + recs = load_observations(str(d)) + assert {r.observation_id for r in recs} == {"a", "b"} diff --git a/backend-compliance/tests/test_onboarding_advisor.py b/backend-compliance/tests/test_onboarding_advisor.py new file mode 100644 index 00000000..c9ad7541 --- /dev/null +++ b/backend-compliance/tests/test_onboarding_advisor.py @@ -0,0 +1,90 @@ +"""Smart Onboarding Advisor — acceptance tests (the 7 criteria). + +A synthetic multi-certified company (ISO 9001 + ISO 27001 + ISO 14001 + TISAX) onboards toward the CRA. +The Advisor orchestrates the existing engines and must satisfy: multi-cert works; ISO 14001 is not +falsely relevant; certs reduce questions but satisfy nothing automatically (Welt-1); <=5 self-explaining +next-best questions; answers update the profile (delta shrinks); sales selects/interprets nothing. +""" + +from __future__ import annotations + +import os + +import yaml + +from compliance.onboarding import OnboardingInput, advisor_start, apply_answer +from compliance.transition_reasoning import TargetRequirement + +_CRA = yaml.safe_load(open(os.path.join( + os.path.dirname(__file__), "..", "knowledge", "transition_patterns", + "transition_pattern_iso27001_to_cra_maschinenvo_v1.yaml"), encoding="utf-8")) +_INFOSEC = [a["capability"] for a in _CRA["likely_covered"]] +_REQ = [TargetRequirement(capability_id=a["capability"]) for a in _CRA["likely_covered"]] +_REQ += [TargetRequirement(capability_id=d["capability"], question_intent=d.get("needed_information", "verify_existence"), + expected_evidence=d.get("expected_evidence", [])) + for d in _CRA["delta_requirements"]] +_COVERS = {d["capability"]: d.get("covers_targets", []) for d in _CRA["delta_requirements"]} + +_HYP = { + "ISO27001": _INFOSEC, + "TISAX": _INFOSEC, + "ISO9001": ["ce_conformity_assessment_and_technical_documentation"], # a CRA delta cap (relevant) + "ISO14001": ["environmental_management_documentation"], # NOT in the CRA required set +} +_INPUT = OnboardingInput( + company="synthetic", industry="machine_builder", products=["parking payment system"], + markets=["EU"], certifications=["ISO9001", "ISO27001", "ISO14001", "TISAX"], + known_evidence=["CE process"], target=["CRA"]) + + +def _run(inp=_INPUT, hyp=_HYP): + return advisor_start(inp, hyp, _REQ, target_id="CRA", covers_targets=_COVERS, + corpus_status={"CRA": "validated"}) + + +def test_1_multi_certification_works(): + res = _run() + certs = {a.certification for a in res.inferred_assumptions} + assert {"ISO27001", "ISO9001"} <= certs # several certs contribute inferred capabilities + + +def test_2_iso14001_not_falsely_relevant_for_cra(): + res = _run() + assert any(r.certification == "ISO14001" for r in res.rejected_assumptions) + assert all(a.certification != "ISO14001" for a in res.inferred_assumptions) + + +def test_3_certs_reduce_questions_but_satisfy_nothing_automatically(): + res = _run() + for a in res.inferred_assumptions: + assert a.verification_required is True + assert "nicht automatisch erfüllt" in a.statement + + +def test_4_at_most_five_next_best_questions(): + res = _run() + assert 0 < len(res.next_best_questions) <= 5 + + +def test_5_every_question_explains_why(): + res = _run() + assert all(q.why.strip() for q in res.next_best_questions) + + +def test_6_each_answer_updates_the_profile(): + res = _run() + open_cap = res.capability_delta[0] + # the answer "confirmed" adds the capability; re-running shrinks the delta + confirmed = apply_answer([], open_cap, "confirmed") + assert confirmed == [open_cap] + hyp2 = {**_HYP, "ANSWERED": confirmed} + inp2 = _INPUT.model_copy(update={"certifications": _INPUT.certifications + ["ANSWERED"]}) + res2 = advisor_start(inp2, hyp2, _REQ, target_id="CRA", covers_targets=_COVERS, corpus_status={"CRA": "validated"}) + assert len(res2.capability_delta) < len(res.capability_delta) + + +def test_7_sales_selects_nothing_engine_produces_everything(): + res = _run() + # from plain inputs the engine produced the whole advisory payload + assert res.headline and res.capability_delta and res.top_measures and res.evidence_requests + assert res.completeness_summary diff --git a/backend-compliance/tests/test_onboarding_endpoint.py b/backend-compliance/tests/test_onboarding_endpoint.py new file mode 100644 index 00000000..0885646f --- /dev/null +++ b/backend-compliance/tests/test_onboarding_endpoint.py @@ -0,0 +1,90 @@ +"""POST /onboarding/advisor-start — the runtime endpoint that exposes the existing Advisor. + +Exercises the router in isolation (no DB, no full app): scanner findings (ProducedSignal) -> normalize +-> Silent Pass -> Advisor -> the advisory payload. No new reasoning logic — just the wiring. +""" + +from __future__ import annotations + +from fastapi import FastAPI +from fastapi.testclient import TestClient + +from compliance.api.onboarding_routes import router + +_app = FastAPI() +_app.include_router(router) +_client = TestClient(_app) + +_BODY = { + "company": "synthetic", "industry": "machine_builder", "products": ["parking payment system"], + "markets": ["EU"], "certifications": ["ISO27001", "ISO9001"], "known_evidence": ["CE process"], + "target": "CRA", + "scanner_findings": [ + {"signal_id": "cyclonedx_found", "source_type": "repository", "evidence": "sbom", "provenance": "sbom.cdx.json"}, + {"signal_id": "vdp_found", "source_type": "website", "provenance": "/.well-known/security.txt"}, + {"signal_id": "risk_assessment_pdf", "source_type": "document", "provenance": "risk.pdf"}, + {"signal_id": "cloud_hosted", "source_type": "product"}, + ], +} + + +def test_targets_endpoint_lists_supported(): + r = _client.get("/onboarding/targets") + assert r.status_code == 200 + assert "CRA" in r.json()["targets"] + + +def test_advisor_start_returns_full_payload(): + r = _client.post("/onboarding/advisor-start", json=_BODY) + assert r.status_code == 200, r.text + d = r.json() + for field in ["silent_intake_summary", "inferred_assumptions", "rejected_assumptions", + "top_5_questions", "capability_delta", "top_measures", "evidence_requests", + "completeness_summary"]: + assert field in d + assert len(d["top_5_questions"]) <= 5 + assert d["auto_detected"] # Silent Pass recognised things from the scanners + assert "sbom_creation" not in {q["capability_id"] for q in d["top_5_questions"]} # detected -> not asked + + +def test_requirement_signal_does_not_auto_detect_capability(): + # a tender that DEMANDS an SBOM (requirement) must NOT be read as "SBOM present": sbom_creation stays + # open (asked / in the delta), unlike a real cyclonedx_found observation. + body = dict(_BODY, scanner_findings=[ + {"signal_id": "requires_sbom", "source_type": "tender", "provenance": "tender §4.2"}, + ]) + r = _client.post("/onboarding/advisor-start", json=body) + assert r.status_code == 200, r.text + d = r.json() + assert "sbom_creation" not in d["auto_detected"] # demanded != present + asked = {q["capability_id"] for q in d["top_5_questions"]} + assert "sbom_creation" in asked or "sbom_creation" in d["capability_delta"] # still an open gap + + +def test_partial_signal_surfaces_as_indication_and_is_still_asked(): + # a PARTIAL observation (a CI pipeline) raises assumption strength but does NOT replace the question + body = dict(_BODY, scanner_findings=[{"signal_id": "github_actions_ci", "source_type": "repository"}]) + r = _client.post("/onboarding/advisor-start", json=body) + assert r.status_code == 200, r.text + d = r.json() + assert "secure_development_lifecycle" not in d["auto_detected"] # partial != detected + assert "secure_development_lifecycle" in d["indications"] # but its strength is shown + asked = {q["capability_id"] for q in d["top_5_questions"]} + assert "secure_development_lifecycle" in asked or "secure_development_lifecycle" in d["capability_delta"] + + +def test_questions_carry_curated_text_and_human_labels(): + # the curated why_asked from the transition pattern must reach the question (not the generic + # fallback "Keine Anhaltspunkte ... klären"), and surfaced capabilities get human labels. + body = dict(_BODY, certifications=["ISO27001"], target="TISAX", scanner_findings=[]) + r = _client.post("/onboarding/advisor-start", json=body) + assert r.status_code == 200, r.text + d = r.json() + assert any("Keine Anhaltspunkte" not in q["why"] for q in d["top_5_questions"]) # real expert text surfaced + assert d["capability_labels"].get("vda_isa_self_assessment") == "VDA-ISA-Selbstauskunft" + + +def test_unknown_target_is_404(): + body = dict(_BODY, target="NOPE") + r = _client.post("/onboarding/advisor-start", json=body) + assert r.status_code == 404 diff --git a/backend-compliance/tests/test_optimization.py b/backend-compliance/tests/test_optimization.py new file mode 100644 index 00000000..a1ca8610 --- /dev/null +++ b/backend-compliance/tests/test_optimization.py @@ -0,0 +1,127 @@ +"""Tests for the Regulatory Optimization renderer (Roadmap / Management view of the Capability Delta). + +Acceptance: rank measures by regulatory leverage (most regulatory requirements closed at once), +report the compression (identified requirements -> measures), answer the budget question, and bind +to the SAME RS-005 Capability Delta the Interview Renderer uses. Percentages are over IDENTIFIED +requirements (Welt-1), never "% gesetzeskonform". +""" + +from __future__ import annotations + +from compliance.optimization import ( + BudgetPlan, OptimizationPlan, regulatory_leverage, roadmap_from_delta, select_within_budget, +) +from compliance.transition_reasoning import ( + CapabilityCoverage, CoverageStatus, TransitionAssessment, TransitionSummary, +) + +# Illustrative leverage spread (the user's patch_management example reaches leverage 4). +CAPS = { + "patch_management": ["CRA", "MaschinenVO", "IEC62443", "ISO27001"], # leverage 4 + "access_control": ["CRA", "ISO27001"], # leverage 2 + "sbom": ["CRA"], # leverage 1 + "machine_guards": ["MaschinenVO"], # leverage 1 +} + + +def test_ranked_by_leverage_desc(): + plan = regulatory_leverage(CAPS) + order = [m.capability_id for m in plan.ranked_measures] + assert order[0] == "patch_management" # leverage 4 first + assert order[1] == "access_control" # leverage 2 next + assert plan.ranked_measures[0].leverage == 4 + + +def test_compression_counts(): + plan = regulatory_leverage(CAPS) + # total requirements = 4 + 2 + 1 + 1 = 8 closed by 4 measures + assert plan.total_requirements == 8 and plan.total_measures == 4 + assert "8 identifizierte Anforderungen" in plan.headline + + +def test_cumulative_coverage_monotone_to_one(): + plan = regulatory_leverage(CAPS) + cums = [m.cumulative_coverage for m in plan.ranked_measures] + assert cums == sorted(cums) # non-decreasing + assert abs(cums[-1] - 1.0) < 1e-9 # full set -> 100% + assert plan.ranked_measures[0].cumulative_requirements == 4 + + +def test_tie_break_deterministic_by_id(): + # machine_guards vs sbom both leverage 1 -> alphabetical: machine_guards before sbom + plan = regulatory_leverage(CAPS) + tail = [m.capability_id for m in plan.ranked_measures if m.leverage == 1] + assert tail == ["machine_guards", "sbom"] + + +def test_budget_picks_highest_leverage(): + b = select_within_budget(CAPS, 2) + assert b.selected_capabilities == ["patch_management", "access_control"] + assert b.requirements_closed == 6 and b.total_requirements == 8 + assert abs(b.coverage_ratio - 0.75) < 1e-9 + assert "6 von 8" in b.headline and "75%" in b.headline + + +def test_budget_over_and_zero(): + assert select_within_budget(CAPS, 99).requirements_closed == 8 # capped at all + z = select_within_budget(CAPS, 0) + assert z.selected_capabilities == [] and z.requirements_closed == 0 + + +def test_in_scope_filter(): + # restrict to CRA + MaschinenVO: patch=2, access=1(CRA), sbom=1, guards=1 -> total 5 + plan = regulatory_leverage(CAPS, in_scope=["CRA", "MaschinenVO"]) + assert plan.total_requirements == 5 + assert plan.ranked_measures[0].capability_id == "patch_management" + assert plan.ranked_measures[0].leverage == 2 # only CRA+MaschinenVO counted + + +def test_deterministic(): + a, b = regulatory_leverage(CAPS), regulatory_leverage(CAPS) + assert [m.capability_id for m in a.ranked_measures] == [m.capability_id for m in b.ranked_measures] + assert a.headline == b.headline + + +def test_empty(): + plan = regulatory_leverage({}) + assert plan.total_requirements == 0 and plan.ranked_measures == [] + assert isinstance(plan, OptimizationPlan) + + +def test_capability_with_no_in_scope_requirement_dropped(): + plan = regulatory_leverage({"x": ["CRA"], "y": ["DataAct"]}, in_scope=["CRA"]) + assert [m.capability_id for m in plan.ranked_measures] == ["x"] # y covers nothing in scope + + +# The keystone: optimization renders the SAME RS-005 delta the interview uses. +def _delta(): + return TransitionAssessment( + target_id="CRA+MaschinenVO", + coverage=[ + CapabilityCoverage(capability_id="patch_management", status=CoverageStatus.MISSING), + CapabilityCoverage(capability_id="sbom", status=CoverageStatus.MISSING), + CapabilityCoverage(capability_id="access_control", status=CoverageStatus.ALREADY_COVERED), + CapabilityCoverage(capability_id="machine_guards", status=CoverageStatus.MISSING), + ], + summary=TransitionSummary(), + ) + + +def test_roadmap_from_delta_uses_only_open_capabilities(): + plan = roadmap_from_delta(_delta(), CAPS) + ids = [m.capability_id for m in plan.ranked_measures] + assert "access_control" not in ids # ALREADY_COVERED is not an open measure + assert ids == ["patch_management", "machine_guards", "sbom"] # MISSING ranked by leverage + assert isinstance(plan, OptimizationPlan) + + +def test_roadmap_from_delta_honours_status_filter(): + # include NEEDS_CONFIRMATION too -> still only those present in CAPS contribute requirements + a = _delta() + a.coverage.append(CapabilityCoverage(capability_id="access_control", status=CoverageStatus.NEEDS_CONFIRMATION)) + plan = roadmap_from_delta(a, CAPS, open_statuses=[CoverageStatus.MISSING, CoverageStatus.NEEDS_CONFIRMATION]) + assert "access_control" in [m.capability_id for m in plan.ranked_measures] + + +def test_budget_returns_budgetplan_type(): + assert isinstance(select_within_budget(CAPS, 1), BudgetPlan) diff --git a/backend-compliance/tests/test_playbook.py b/backend-compliance/tests/test_playbook.py new file mode 100644 index 00000000..158997f5 --- /dev/null +++ b/backend-compliance/tests/test_playbook.py @@ -0,0 +1,88 @@ +"""Tests for the Implementation Playbook renderer (the Berater view, "wie komme ich dort hin?"). + +Acceptance: for one capability, assemble the journey (why / closes-which-regulations / tools / +process / evidence / controls) from curated knowledge + leverage + injected Execution links; chain +the Optimization Roadmap into per-measure playbooks; surface capabilities without content as honest +`missing` stubs. Curated content is an expert draft, never normative. +""" + +from __future__ import annotations + +from compliance.optimization import regulatory_leverage +from compliance.playbook import Playbook, build_playbook, playbooks_for_plan + +SBOM = { + "title": "SBOM aufbauen", + "why": "CRA verlangt ein Komponenten-Inventar.", + "tools": ["CycloneDX", "Syft"], + "process_steps": [{"title": "Format wählen", "detail": "CycloneDX"}, {"title": "In CI erzeugen", "detail": ""}], + "expected_evidence": ["sbom_per_release"], + "how_others_do_it": "CI + Dependency-Track.", + "status": "draft", +} + + +def test_build_playbook_full_journey(): + pb = build_playbook("sbom_creation", SBOM, closes_regulations=["CRA", "MaschinenVO"], control_links=["component_inventory"]) + assert pb.title == "SBOM aufbauen" and pb.status == "draft" + assert pb.closes_regulations == ["CRA", "MaschinenVO"] and pb.leverage == 2 + assert pb.tools == ["CycloneDX", "Syft"] + assert [s.order for s in pb.process_steps] == [1, 2] + assert pb.process_steps[0].title == "Format wählen" + assert pb.expected_evidence == ["sbom_per_release"] + assert pb.controls == ["component_inventory"] # injected from Execution + assert pb.disclaimer # always carries the expert-draft caveat + + +def test_missing_knowledge_is_honest_stub(): + pb = build_playbook("product_cyber_risk_assessment", None, closes_regulations=["CRA", "MaschinenVO"]) + assert pb.status == "missing" # the content-owed signal + assert pb.leverage == 2 and pb.closes_regulations == ["CRA", "MaschinenVO"] # leverage still known + assert pb.tools == [] and pb.process_steps == [] + assert "Knowledge Acquisition" in pb.why + + +def test_closes_regulations_deduped_and_sorted(): + pb = build_playbook("x", SBOM, closes_regulations=["MaschinenVO", "CRA", "CRA"]) + assert pb.closes_regulations == ["CRA", "MaschinenVO"] and pb.leverage == 2 + + +def test_controls_default_empty_no_execution_data(): + pb = build_playbook("x", SBOM, closes_regulations=["CRA"]) + assert pb.controls == [] # no Execution data unless injected + + +def test_playbooks_for_plan_orders_by_leverage_and_stubs_missing(): + caps = {"sbom_creation": ["CRA"], "pcra": ["CRA", "MaschinenVO"], "guards": ["MaschinenVO"]} + plan = regulatory_leverage(caps) + pbs = playbooks_for_plan(plan, {"sbom_creation": SBOM}, top_k=3) + assert [p.capability_id for p in pbs][0] == "pcra" # highest leverage first + by = {p.capability_id: p for p in pbs} + assert by["pcra"].status == "missing" and by["pcra"].leverage == 2 + assert by["sbom_creation"].status == "draft" # has content + assert by["guards"].status == "missing" + + +def test_playbooks_for_plan_top_k_and_injected_controls(): + caps = {"a": ["CRA", "MaschinenVO"], "b": ["CRA"], "c": ["CRA"]} + plan = regulatory_leverage(caps) + pbs = playbooks_for_plan(plan, {}, top_k=1, control_links_by_cap={"a": ["ctrl_1"]}) + assert len(pbs) == 1 and pbs[0].capability_id == "a" + assert pbs[0].controls == ["ctrl_1"] + + +def test_playbooks_for_plan_empty(): + plan = regulatory_leverage({}) + assert playbooks_for_plan(plan, {}) == [] + + +def test_deterministic(): + caps = {"a": ["CRA", "MaschinenVO"], "b": ["CRA"]} + plan = regulatory_leverage(caps) + a = [p.capability_id for p in playbooks_for_plan(plan, {})] + b = [p.capability_id for p in playbooks_for_plan(plan, {})] + assert a == b + + +def test_returns_playbook_type(): + assert isinstance(build_playbook("x", None), Playbook) diff --git a/backend-compliance/tests/test_signal_producer.py b/backend-compliance/tests/test_signal_producer.py new file mode 100644 index 00000000..0b0808b8 --- /dev/null +++ b/backend-compliance/tests/test_signal_producer.py @@ -0,0 +1,84 @@ +"""Signal Producer + Normalizer — one signal language, but TWO signal KINDS. + +Pins the abstraction: every source emits the same ProducedSignal, and the Normalizer reduces +producer-specific ids to ONE canonical signal via a vocabulary. CRITICAL: an OBSERVATION ("I saw an +SBOM") and a REQUIREMENT ("a tender DEMANDS an SBOM") must NEVER collapse to the same signal — a +demanded SBOM is not a present one. kind is authoritative on the canonical vocabulary entry, and the +Silent Pass consumes only observations. +""" + +from __future__ import annotations + +import os + +import yaml + +from compliance.onboarding import ( + ProducedSignal, + SignalMapping, + SignalVocabularyEntry, + normalize_signals, + silent_intake, +) + +_DIR = os.path.dirname(__file__) +_VOCAB = [SignalVocabularyEntry(**v) for v in yaml.safe_load( + open(os.path.join(_DIR, "..", "knowledge", "onboarding", "signal_vocabulary.yaml"), encoding="utf-8"))["signals"]] +_MAP = [SignalMapping(**m) for m in yaml.safe_load( + open(os.path.join(_DIR, "..", "knowledge", "onboarding", "intake_signal_map.yaml"), encoding="utf-8"))["mappings"]] + + +def test_observation_producers_yield_one_canonical_signal(): + # the SAME OBSERVATION, emitted by three different producers with different raw ids + produced = [ + ProducedSignal(signal_id="cyclonedx_found", source_type="repository", provenance="sbom.cdx.json"), + ProducedSignal(signal_id="spdx_found", source_type="repository", provenance="sbom.spdx"), + ProducedSignal(signal_id="sbom_uploaded", source_type="document", provenance="customer_upload.pdf"), + ] + normalized = normalize_signals(produced, _VOCAB) + assert {s.signal for s in normalized} == {"sbom_present"} # all reduced to ONE canonical observation + assert {s.kind for s in normalized} == {"observation"} # all observations + assert {s.source for s in normalized} == {"repository", "document"} # provenance preserved + + +def test_requirement_and_observation_never_collapse(): + # a tender that DEMANDS an SBOM must NOT become the same signal as a repo that HAS one + normalized = normalize_signals([ + ProducedSignal(signal_id="cyclonedx_found", source_type="repository"), # observation + ProducedSignal(signal_id="requires_sbom", source_type="tender", provenance="tender §4.2"), # requirement + ], _VOCAB) + by_kind = {s.kind: s.signal for s in normalized} + assert by_kind["observation"] == "sbom_present" + assert by_kind["requirement"] == "sbom_required" + assert by_kind["observation"] != by_kind["requirement"] + + +def test_requirement_signal_produces_no_capability(): + # the regression the whole fix is about: a DEMANDED SBOM yields NO detected capability, + # but is preserved as a requirement; a real SBOM in the repo still IS detected. + from_tender = normalize_signals([ProducedSignal(signal_id="requires_sbom", source_type="tender")], _VOCAB) + res_tender = silent_intake(from_tender, _MAP) + assert res_tender.capability_ids() == [] # NOT read as present + assert res_tender.requirements_seen == ["sbom_required"] # but preserved + visible + + from_repo = normalize_signals([ProducedSignal(signal_id="cyclonedx_found", source_type="repository", evidence="sbom")], _VOCAB) + assert silent_intake(from_repo, _MAP).capability_ids() == ["sbom_creation"] + + +def test_vocabulary_kind_overrides_a_mislabelled_producer(): + # even if a producer wrongly tags a requirement as observation, the vocabulary is authoritative + norm = normalize_signals([ProducedSignal(signal_id="requires_sbom", source_type="tender", kind="observation")], _VOCAB) + assert norm[0].signal == "sbom_required" and norm[0].kind == "requirement" + + +def test_unknown_signal_passes_through_not_dropped(): + out = normalize_signals([ProducedSignal(signal_id="brand_new_scanner_signal", source_type="api")], _VOCAB) + assert out[0].signal == "brand_new_scanner_signal" # visible, not silently lost + + +def test_confidence_and_provenance_flow_to_detected_capability(): + norm = normalize_signals([ProducedSignal(signal_id="security_txt", source_type="website", + confidence=0.8, evidence="cvd_policy", provenance="/.well-known/security.txt")], _VOCAB) + res = silent_intake(norm, _MAP) + d = next(d for d in res.detected_capabilities if d.capability == "coordinated_vulnerability_disclosure") + assert d.confidence == 0.8 and d.provenance == "/.well-known/security.txt" diff --git a/backend-compliance/tests/test_silent_intake.py b/backend-compliance/tests/test_silent_intake.py new file mode 100644 index 00000000..54ea7996 --- /dev/null +++ b/backend-compliance/tests/test_silent_intake.py @@ -0,0 +1,101 @@ +"""Silent Knowledge Pass — recognise before asking (Phase 0). + +Pins the deterministic signal->capability/product-fact mapping and the product effect that matters: when +the Silent Pass feeds detected capabilities into the Advisor, the delta shrinks and the number of +next-best questions DROPS — "we already recognised X, only these few remain" instead of a question wall. +""" + +from __future__ import annotations + +import os + +import yaml + +from compliance.onboarding import ( + IntakeSignal, + OnboardingInput, + SignalMapping, + advisor_start, + resolve_for_certifications, + silent_intake, +) +from compliance.onboarding import CapabilityHypothesis +from compliance.transition_reasoning import TargetRequirement + +_DIR = os.path.dirname(__file__) +_MAP = [SignalMapping(**m) for m in yaml.safe_load( + open(os.path.join(_DIR, "..", "knowledge", "onboarding", "intake_signal_map.yaml"), encoding="utf-8"))["mappings"]] +_LIB = [CapabilityHypothesis(**h) for h in yaml.safe_load( + open(os.path.join(_DIR, "..", "knowledge", "certification_hypotheses", "hypotheses.yaml"), encoding="utf-8"))["hypotheses"]] +_CRA = yaml.safe_load(open(os.path.join(_DIR, "..", "knowledge", "transition_patterns", + "transition_pattern_iso27001_to_cra_maschinenvo_v1.yaml"), encoding="utf-8")) +_REQ = [TargetRequirement(capability_id=a["capability"]) for a in _CRA["likely_covered"]] +_REQ += [TargetRequirement(capability_id=d["capability"], expected_evidence=d.get("expected_evidence", [])) + for d in _CRA["delta_requirements"]] + +# scanner findings (injected): a machine builder with a public CVD policy, an SBOM + signed releases in +# the repo, a product risk-assessment doc, and a cloud-connected PLC product. +_SIGNALS = [ + IntakeSignal(source="website", signal="cvd_policy_present", detail="/.well-known/security.txt"), + IntakeSignal(source="repository", signal="sbom_present", detail="sbom.cdx.json"), + IntakeSignal(source="repository", signal="signed_releases"), + IntakeSignal(source="document", signal="product_risk_assessment_doc"), + IntakeSignal(source="product", signal="cloud_connectivity"), + IntakeSignal(source="product", signal="plc_sps"), +] + + +def test_silent_intake_is_deterministic_signal_mapping(): + res = silent_intake(_SIGNALS, _MAP) + caps = set(res.capability_ids()) + assert {"coordinated_vulnerability_disclosure", "sbom_creation", "secure_signed_update_distribution", + "product_cyber_risk_assessment"} <= caps + assert "sbom" in res.evidence_found # evidence already in hand -> no upload needed + facts = {f.key for f in res.product_facts} + assert "connected_to_internet" in facts and "is_machine" in facts + + +def test_silent_pass_reduces_the_questions(): + inp = OnboardingInput(company="x", certifications=["ISO27001", "ISO9001"], target=["CRA"]) + hyp = resolve_for_certifications(inp.certifications, _LIB) + without = advisor_start(inp, hyp, _REQ, target_id="CRA", corpus_status={"CRA": "validated"}) + detected = silent_intake(_SIGNALS, _MAP).capability_ids() + with_pass = advisor_start(inp, hyp, _REQ, target_id="CRA", corpus_status={"CRA": "validated"}, + detected_capabilities=detected) + # the whole point: recognising things automatically leaves FEWER open questions + assert len(with_pass.capability_delta) < len(without.capability_delta) + assert len(with_pass.next_best_questions) <= len(without.next_best_questions) + assert with_pass.auto_detected # recognised without asking + assert "automatisch erkannt (Intake)" in with_pass.headline + + +def test_detected_capabilities_are_not_asked_again(): + inp = OnboardingInput(company="x", certifications=["ISO27001"], target=["CRA"]) + hyp = resolve_for_certifications(inp.certifications, _LIB) + detected = silent_intake(_SIGNALS, _MAP).capability_ids() + res = advisor_start(inp, hyp, _REQ, target_id="CRA", corpus_status={"CRA": "validated"}, + detected_capabilities=detected) + asked = {q.capability_id for q in res.next_best_questions} + assert "sbom_creation" not in asked and "sbom_creation" not in res.capability_delta + + +def test_partial_signal_is_indicative_not_detected(): + # a PARTIAL signal (CI present -> secure dev lifecycle) raises assumption strength but is NOT a + # detected capability: it must NOT shrink the delta the way a concrete artifact does. + res = silent_intake([IntakeSignal(source="repository", signal="github_actions_ci")], _MAP) + assert "secure_development_lifecycle" not in res.capability_ids() # not counted as present + assert res.indicative_capability_ids() == ["secure_development_lifecycle"] # surfaced as an indication + # the summary counts detected and indications SEPARATELY (no over-claim of "automatisch erkannt") + assert "0 Fähigkeit(en) automatisch erkannt, 1 Indikation(en)" in res.summary + + +def test_partial_indication_does_not_remove_the_question(): + inp = OnboardingInput(company="x", certifications=["ISO27001"], target=["CRA"]) + hyp = resolve_for_certifications(inp.certifications, _LIB) + si = silent_intake([IntakeSignal(source="repository", signal="github_actions_ci")], _MAP) + res = advisor_start(inp, hyp, _REQ, target_id="CRA", corpus_status={"CRA": "validated"}, + detected_capabilities=si.capability_ids(), + indicative_capabilities=si.indicative_capability_ids()) + assert "secure_development_lifecycle" not in res.auto_detected # partial != detected + assert "secure_development_lifecycle" in res.indications # strength shown + assert "secure_development_lifecycle" in res.capability_delta # gap still open / asked diff --git a/backend-compliance/tests/test_transition_reasoning.py b/backend-compliance/tests/test_transition_reasoning.py new file mode 100644 index 00000000..14822b93 --- /dev/null +++ b/backend-compliance/tests/test_transition_reasoning.py @@ -0,0 +1,145 @@ +"""Tests for Transition Reasoning v0 (RS-005) — the Transition Planning Engine. + +Acceptance: from a TransitionGoal + the Company Capability Profile (2A, „have") + +INJECTED TargetRequirements (Execution-owned „required"), the engine emits ranked +`TransitionQuestionRequest`s (information gaps) — NOT rendered questions. A +certification-derived capability is „probably_covered" (Welt 1), never „already_covered". + +The cert->capability mapping below is a MOCK (Execution-owned in reality), only here. +""" + +from __future__ import annotations + +from compliance.company import ( + CapabilityMappingEntry, Certification, CompanyContext, Declaration, + ExistingEvidence, build_company_profile, +) +from compliance.reasoning.enums import Confidence +from compliance.transition_reasoning import ( + CoverageStatus, InformationGain, RequestPriority, TargetRequirement, TargetType, + TransitionContext, TransitionGoal, assess_transition, +) + +ISO_MAP = {"ISO27001": CapabilityMappingEntry( + capability_ids=["cap_incident_response", "cap_supplier_management"], confidence=Confidence.MEDIUM)} + + +def _profile(): + ctx = CompanyContext( + company_id="kunde", + certifications=[Certification(certification_id="ISO27001")], + declarations=[Declaration(capability_id="cap_asset_management")], + evidence=[ExistingEvidence(evidence_id="patch.pdf", evidence_type="policy", proves_capability_id="cap_patch_management")], + ) + return build_company_profile(ctx, ISO_MAP) + + +def _ctx(): + return TransitionContext(company_id="kunde", known_certifications=["ISO27001"], + target=TransitionGoal(target_id="CRA", target_type=TargetType.REGULATION)) + + +# CRA-Required (injiziert; in echt: Obligation->Control->Required Capability, Execution) +def _reqs(): + return [ + TargetRequirement(capability_id="cap_patch_management", expected_evidence=["policy"]), # confirmed + TargetRequirement(capability_id="cap_incident_response"), # inferred (ISO) + TargetRequirement(capability_id="cap_asset_management"), # declared + TargetRequirement(capability_id="cap_sbom", question_intent="verify_existence", expected_evidence=["sbom"]), # missing + TargetRequirement(capability_id="cap_vuln_handling", supports_obligations=["CRA.1", "CRA.2"]), # missing, 2 obligations + TargetRequirement(capability_id="cap_wastewater", unsupported=True), # not in corpus + ] + + +def _req_ids(a): + return [r.capability_id for r in a.question_requests] + + +def _cov(a, cap): + return [c for c in a.coverage if c.capability_id == cap][0] + + +# The engine emits REQUESTS (information gaps), not rendered questions. +def test_emits_requests_not_questions(): + a = assess_transition(_ctx(), _reqs(), _profile()) + r = a.question_requests[0] + assert r.capability_id and r.question_intent and r.priority + # NO rendered question text anywhere — rendering is RS-005.1, not this engine + assert not hasattr(r, "question") + assert "question" not in type(r).model_fields and "rendered_text" not in type(r).model_fields + + +# Confirmed capability -> already_covered, NO request. +def test_confirmed_already_covered_no_request(): + a = assess_transition(_ctx(), _reqs(), _profile()) + assert _cov(a, "cap_patch_management").status == CoverageStatus.ALREADY_COVERED + assert "cap_patch_management" not in _req_ids(a) + + +# A certification-inferred capability is PROBABLY_COVERED (Welt 1), not already_covered; +# it produces a confirmation request, never a verdict. +def test_certification_inferred_is_probable_with_confirm_request(): + a = assess_transition(_ctx(), _reqs(), _profile()) + c = _cov(a, "cap_incident_response") + assert c.status == CoverageStatus.PROBABLY_COVERED + assert c.status != CoverageStatus.ALREADY_COVERED # cert alone never „erfüllt" + req = [r for r in a.question_requests if r.capability_id == "cap_incident_response"][0] + assert req.priority == RequestPriority.MEDIUM + + +# A missing required capability -> high-priority request. +def test_missing_high_priority_request(): + a = assess_transition(_ctx(), _reqs(), _profile()) + assert _cov(a, "cap_sbom").status == CoverageStatus.MISSING + req = [r for r in a.question_requests if r.capability_id == "cap_sbom"][0] + assert req.priority == RequestPriority.HIGH and req.information_gain == InformationGain.HIGH + + +# An unsupported domain -> no request (future corpus, honest). +def test_unsupported_no_request(): + a = assess_transition(_ctx(), _reqs(), _profile()) + assert _cov(a, "cap_wastewater").status == CoverageStatus.UNSUPPORTED + assert "cap_wastewater" not in _req_ids(a) + + +# Requests are ranked: HIGH (missing) before MEDIUM (probable/declared). +def test_requests_ranked_high_before_medium(): + a = assess_transition(_ctx(), _reqs(), _profile()) + prios = [r.priority for r in a.question_requests] + assert prios == sorted(prios, key=lambda p: {RequestPriority.HIGH: 0, RequestPriority.MEDIUM: 1, RequestPriority.LOW: 2}[p]) + # the two missing caps come first + assert set(_req_ids(a)[:2]) == {"cap_sbom", "cap_vuln_handling"} + + +# The funnel: certs reduce the open questions (only 4 of 6 requirements need clarifying). +def test_funnel_reduces_open_questions(): + a = assess_transition(_ctx(), _reqs(), _profile()) + # already_covered (patch) + unsupported (wastewater) drop out -> 4 requests + assert len(a.question_requests) == 4 + assert "%" not in a.summary.headline + + +# Deterministic + activates 2A: same inputs -> same result. +def test_deterministic(): + p = _profile() + a1 = assess_transition(_ctx(), _reqs(), p) + a2 = assess_transition(_ctx(), _reqs(), p) + assert _req_ids(a1) == _req_ids(a2) and a1.summary.headline == a2.summary.headline + + +# No requirements / no profile -> empty assessment (no Execution data in product code). +def test_empty(): + a = assess_transition(_ctx()) + assert a.question_requests == [] and a.coverage == [] + + +# Cross-Regulation Capability Mapping: count capabilities that cover >= 2 regulations. +def test_regulatory_convergence(): + from compliance.transition_reasoning import regulatory_convergence + + ct = {"a": ["CRA"], "b": ["CRA", "MaschinenVO"], "c": ["MaschinenVO"], "d": ["CRA", "MaschinenVO"]} + c = regulatory_convergence(ct, ["CRA", "MaschinenVO"]) + assert set(c.multi_target_capabilities) == {"b", "d"} # cover BOTH + assert set(c.single_target_capabilities) == {"a", "c"} + assert c.per_target_count == {"CRA": 3, "MaschinenVO": 3} + assert c.headline.startswith("2 von 4") diff --git a/backend-compliance/tests/test_vocabulary.py b/backend-compliance/tests/test_vocabulary.py new file mode 100644 index 00000000..35e031c4 --- /dev/null +++ b/backend-compliance/tests/test_vocabulary.py @@ -0,0 +1,86 @@ +"""Characterization tests for the Domain Vocabulary (data, not code). + +Pins the IDENTITY-vs-REPRESENTATION contract: regulations have a stable id + canonical name + aliases +(so CRA and "Cyber Resilience Act" resolve to the SAME identity — the normalization that the KPIs +flagged). Journey classes cluster transition instances so we do not duplicate the same reise; they +are PROVISIONAL (no MJRN minting) and reference regulation ids that exist in the vocabulary. +""" + +from __future__ import annotations + +import os + +import yaml + +_VOCAB = os.path.join(os.path.dirname(__file__), "..", "knowledge", "vocabulary") + + +def _regs(): + with open(os.path.join(_VOCAB, "regulations.yaml"), encoding="utf-8") as h: + return yaml.safe_load(h)["regulations"] + + +def _classes(): + with open(os.path.join(_VOCAB, "journey_classes.yaml"), encoding="utf-8") as h: + return yaml.safe_load(h) + + +def _norm(s): + return "".join(c for c in str(s).lower() if c.isalnum()) + + +def _alias_map(): + amap = {} + for r in _regs(): + for name in [r["canonical"]] + list(r.get("aliases", [])): + amap[_norm(name)] = r["id"] + return amap + + +def test_every_regulation_has_id_canonical_aliases(): + for r in _regs(): + assert r["id"] and r["canonical"] and r["aliases"] + assert r["id"] == r["id"].lower() # ids are lowercase stable keys + + +def test_cra_spellings_resolve_to_one_identity(): + amap = _alias_map() + # the exact normalization the KPIs needed: CRA == Cyber Resilience Act + assert amap[_norm("CRA")] == "cra" and amap[_norm("Cyber Resilience Act")] == "cra" + assert amap[_norm("Regulation (EU) 2024/2847")] == "cra" + + +def test_iso_and_management_system_aliases_resolve(): + amap = _alias_map() + assert amap[_norm("ISO9001")] == "iso9001" and amap[_norm("QMS")] == "iso9001" + assert amap[_norm("ISO/IEC 27001")] == "iso27001" and amap[_norm("ISMS")] == "iso27001" + assert amap[_norm("Maschinenverordnung")] == "maschinenvo" and amap[_norm("MaschinenVO")] == "maschinenvo" + + +def test_aliases_are_unambiguous(): + # no normalized alias maps to two different regulation identities + seen = {} + for r in _regs(): + for name in [r["canonical"]] + list(r.get("aliases", [])): + k = _norm(name) + assert seen.get(k, r["id"]) == r["id"], "ambiguous alias %r" % name + seen[k] = r["id"] + + +def test_journey_classes_are_provisional(): + assert _classes()["status"] == "provisional" # new abstraction -> own Rule of Three + + +def test_iso9001_maschinenvo_is_an_instance_not_a_new_kind(): + classes = _classes()["classes"] + qm = [c for c in classes if c["id"] == "qm-to-product-compliance"][0] + pairs = {(i["from"], i["to"]) for i in qm["instances"]} + assert ("iso9001", "maschinenvo") in pairs # same CLASS as iso9001->cra, iso13485->mdr + assert ("iso13485", "mdr") in pairs # class generalises across domains + + +def test_class_endpoints_reference_known_regulations(): + reg_ids = {r["id"] for r in _regs()} + for c in _classes()["classes"]: + for inst in c["instances"]: + assert inst["from"] in reg_ids and inst["to"] in reg_ids # vocabulary is internally consistent diff --git a/docs-src/architecture/adr/ADR-001-runtime-deploy-policy.md b/docs-src/architecture/adr/ADR-001-runtime-deploy-policy.md new file mode 100644 index 00000000..f5204bb0 --- /dev/null +++ b/docs-src/architecture/adr/ADR-001-runtime-deploy-policy.md @@ -0,0 +1,63 @@ +# ADR-001: Runtime Deploy Policy + +- **Status:** Accepted +- **Datum:** 2026-06-27 +- **Typ:** Architektur-/Release-Entscheidung (keine Entwicklerkonvention) + +## Kernsatz + +> Ein dev-Deploy muss immer einen verifizierbaren Runtime-Effekt haben. +> +> Auf die Frage „warum wurde dieser Build ausgelöst?" muss die Antwort immer lauten können: „weil sich das Laufzeitverhalten geändert hat." + +## Kontext + +Der dev-Deploy (Orca) wird durch einen Push auf den `gitea`-Remote (`gitea.meghsakha.com`) +ausgelöst und baut das Backend als amd64-Image (~5–15 min). In der Praxis entstehen regelmäßig +Änderungen **ohne** Laufzeit-Effekt: Dokumentation, Architekturtexte, Ownership-Modelle, +Koordinations-/Board-Dateien und Referenzartefakte (z. B. die Reference Scenario Suite unter +`backend-compliance/reference_scenarios/`, die vom Backend nicht importiert wird). + +Würde jeder solche Commit deployt, verliert die CI/CD-Historie ihre Aussagekraft: Ein Build +entspräche dann nicht mehr eindeutig einer Laufzeit-Änderung, und die Frage „warum wurde dieser +Build ausgelöst?" ließe sich nicht mehr verlässlich beantworten. + +## Entscheidung + +**Ein dev-Deploy (Orca-Build) wird nur ausgelöst, wenn die Änderung einen verifizierbaren +Laufzeit-Effekt hat.** + +Deployen, wenn **mindestens eines** zutrifft: + +- Runtime-Code geändert +- öffentliche API geändert (Pfad/Methode/Status/Request-/Response-Schema) +- Datenmodell geändert +- Reasoning-Verhalten geändert +- Sicherheitsfix + +**Nicht** deployen bei (nicht abschließend): + +- Dokumentation +- Reference Suites (z. B. `backend-compliance/reference_scenarios/`) +- ADRs (inkl. dieser) +- Board-/Koordinationsdateien +- Ownership-/Architektur-Texte + +Solche Änderungen werden auf `origin/main` gemergt (via PR, **kein** direkter Push), aber **nicht** +auf den `gitea`-Remote gepusht. `origin/main` darf dem dev-Stand damit vorauslaufen; die Parität +wird beim nächsten echten Runtime-Schnitt mitgezogen. + +## Konsequenzen + +- Die CI/CD-Historie bleibt aussagekräftig: jeder Build entspricht einer Laufzeit-Änderung. +- Build-Zeit und Ressourcen werden gespart. +- `origin/main` (Source of Truth) und dev (Laufzeit) dürfen temporär divergieren — das ist + **beabsichtigt und kein Defekt**. Der `last-build/main`-Tag auf dem `gitea`-Remote bezeichnet den + zuletzt deployten Runtime-Stand. +- Vor jedem Push auf `gitea` ist zu prüfen, ob der Diff einen Laufzeit-Effekt hat. + +## Beispiel + +Die Reference Scenario Suite v1 (`8a51db92`) und das Ownership-Dokument +`session_ownership_model_v1.md` (`c7339e68`) wurden auf `origin/main` gemergt, aber bewusst **nicht** +auf dev deployt — beide ohne Laufzeit-Effekt. Diese ADR selbst fällt unter dieselbe Regel. diff --git a/docs-src/architecture/adr/ADR-002-transition-is-data-not-architecture.md b/docs-src/architecture/adr/ADR-002-transition-is-data-not-architecture.md new file mode 100644 index 00000000..1f72e88c --- /dev/null +++ b/docs-src/architecture/adr/ADR-002-transition-is-data-not-architecture.md @@ -0,0 +1,35 @@ +# ADR-002: Transition = Data, not Architecture + +- **Status:** Accepted +- **Datum:** 2026-06-27 +- **Typ:** Architektur-Entscheidung +- **Bezug:** [`../transition-reasoning-spec-v1.md`](../transition-reasoning-spec-v1.md), [[regulatory-intelligence-vision]], Architektur-Freeze v1.0 + +## Kontext + +BreakPilot wird von einem Compliance-Fragebogen zu einer **Transition Engine**: sie beantwortet +`Ausgangszustand → Zielzustand → Delta` (z. B. ISO 27001 → CRA, ISMS → TISAX, MaschRL → MaschVO). +Das Risiko: jede neue regulatorische „Reise" als Engine- oder Metamodell-Erweiterung zu bauen — das +würde die Architektur mit jeder Transition aufblähen und genau den Effekt erzeugen, den der +Architektur-Freeze verhindern soll. + +## Entscheidung + +1. **BreakPilot modelliert keine vollständigen Regelwerke als Interviews — sondern ausschließlich + den minimalen Informationsgewinn, der nötig ist, um einen vorhandenen Unternehmenszustand in + einen gewünschten regulatorischen Zielzustand zu überführen.** + +2. **Jede neue Transition (z. B. ISO 27001 → CRA oder ISMS → TISAX) muss ausschließlich durch neue + Transition Patterns und Capability-/MDQ-Wissen (Daten) entstehen. Weder die Engine noch das + Metamodell dürfen dafür erweitert werden.** + +## Konsequenzen + +- Jede neue regulatorische Reise ist ein **Datenproblem**, kein Architekturproblem — exakt das Ziel + des Architektur-Freeze. +- Eine neue Transition besteht aus: neuen/wiederverwendeten **Master Delta Questions** (MDQ Registry), + einem **Transition Pattern** (nur MDQ-Referenzen) und **Required-Capability-Wissen** (Compliance + Execution). Kein neuer Code im Reasoning-Kern, keine neue Objektklasse im Metamodell. +- Wiederverwendung wird zum Normalfall: `IEC 62443 → CRA` teilt die meisten MDQs mit `ISO 27001 → CRA` + und ergänzt nur wenige neue. +- Diese ADR ist non-runtime → kein Deploy (siehe [ADR-001](ADR-001-runtime-deploy-policy.md)). diff --git a/docs-src/architecture/adr/ADR-003-capability-delta-engine-with-renderers.md b/docs-src/architecture/adr/ADR-003-capability-delta-engine-with-renderers.md new file mode 100644 index 00000000..c08814f2 --- /dev/null +++ b/docs-src/architecture/adr/ADR-003-capability-delta-engine-with-renderers.md @@ -0,0 +1,56 @@ +# ADR-003: Capability Delta Engine — one delta, many renderers + +- **Status:** Accepted +- **Datum:** 2026-06-27 +- **Typ:** Architektur-Entscheidung +- **Bezug:** [ADR-002](ADR-002-transition-is-data-not-architecture.md), [`../transition-reasoning-spec-v1.md`](../transition-reasoning-spec-v1.md), Architektur-Freeze v1.0, [[transition-reasoning]], [[regulatory-intelligence-vision]] + +## Kontext + +GAP-Analyse („Was fehlt mir / welche Informationen brauche ich noch?") und +Maßnahmenpriorisierung („Womit soll ich anfangen?") wurden bisher als zwei Features gedacht. +Sie sind aber **dieselbe Berechnung**: + +``` +Required Capabilities − Known Capabilities = Capability Delta +``` + +`Known` entsteht aus Company Profile + Zertifizierungen + Nachweisen + beantworteten Fragen +(Company 2A). `Required` entsteht aus den Zielregelwerken (CRA, MaschinenVO, Data Act, …). +Die Differenz ist das **Capability Delta**. Erst beim Output verzweigt es sich nach Zielgruppe. + +Das Risiko: das Delta in mehreren Engines getrennt neu zu berechnen (eine „Gap Engine", eine +„Roadmap Engine"). Dann driften die Sichten auseinander und es gibt mehr als eine Wahrheit. + +## Entscheidung + +1. **Es gibt genau EINE Capability Delta Engine** (`compliance/transition_reasoning`, RS-005). + Sie berechnet `Required − Known = Capability Delta` ein einziges Mal. + +2. **Alle Zielgruppen-Outputs sind RENDERER über demselben Delta — keine zweite Berechnung:** + - **Interview Renderer** — fehlende *Informationen* → Fragen (`TransitionQuestionRequest`, gebaut). + - **Roadmap / Management Renderer** — fehlende *Capabilities* → Maßnahmen nach regulatorischem + Hebel (`compliance/optimization`, gebaut). + - **Evidence Renderer** — fehlende *Evidence* → Nachweis-Upload (später). + - **(Ticket/Control Renderer)** — fehlende *Controls* → Tickets (später). + +3. **Abhängigkeitsrichtung:** Renderer hängen von der Delta-Engine ab, nie umgekehrt + (`optimization → transition_reasoning`, azyklisch). Die Delta-Engine bleibt hermetisch + (0 Fremd-Import), damit sie die einzige Quelle der Wahrheit bleibt. + +4. **Begriff:** „Gap" → **„Capability Delta"**. Es beschreibt präziser, was berechnet wird + (eine Differenz von Fähigkeiten), und trägt durch alle Renderer. + +## Konsequenzen + +- **Eine Wahrheit, viele Sichten.** Jede neue Capability, jedes neue Regelwerk und jeder neue + Nachweis verbessert automatisch ALLE Renderer gleichzeitig — kein Sicht-Drift. +- **Kundenreise in drei Fragen, eine Datenbasis:** (1) *Was gilt für mich?* → Reasoning/Scope → + (2) *Was fehlt mir?* → Capability Delta → (3) *Womit anfangen?* → Optimierungsplan. +- **Regulatory Leverage** (Roadmap-Renderer): `leverage(Maßnahme) = Anzahl Regelwerke/Anforderungen, + die sie gleichzeitig schließt`. Ranking nach Hebel + kumulative Abdeckung + Budget-Auswahl. +- **Welt-1-Disziplin:** der Prozentwert des Roadmap-Renderers ist ein exakter Abzählwert über die + **identifizierten** Anforderungen (bekanntes Delta), **kein** „% gesetzeskonform" (Welt 2). +- **Freeze-konform:** kein neues Metamodell, kein neuer Graph — Renderer sind reine, deterministische + Sichten (computed-not-stored). Neue Regelwerke bleiben ein Datenproblem (ADR-002). +- Diese ADR ist non-runtime → kein Deploy (siehe [ADR-001](ADR-001-runtime-deploy-policy.md)). diff --git a/docs-src/architecture/adr/ADR-004-implementation-playbooks.md b/docs-src/architecture/adr/ADR-004-implementation-playbooks.md new file mode 100644 index 00000000..cb123f4c --- /dev/null +++ b/docs-src/architecture/adr/ADR-004-implementation-playbooks.md @@ -0,0 +1,51 @@ +# ADR-004: Implementation Playbooks — a renderer plus a knowledge layer + +- **Status:** Accepted +- **Datum:** 2026-06-27 +- **Typ:** Architektur-Entscheidung +- **Bezug:** [ADR-003](ADR-003-capability-delta-engine-with-renderers.md), [ADR-002](ADR-002-transition-is-data-not-architecture.md), Architektur-Freeze v1.0, [[transition-reasoning]] + +## Kontext + +BreakPilot beantwortet inzwischen drei Fragen: *Was gilt?* (Reasoning), *Was fehlt?* +(Capability Delta), *Womit anfangen?* (Optimization). Danach fragt der Geschäftsführer fast immer: +**„Wie komme ich dort hin?"** — nicht *was*, sondern *wie* (z. B. „Top-Maßnahme: PSIRT — und wie +baue ich einen PSIRT auf? Welche Tools? Wie machen das andere?"). + +Diese Umsetzungs-Ebene ist weder Reasoning noch Gap noch Roadmap. Das Risiko: sie als neue Engine +zu bauen — obwohl ~95 % der Daten bereits existieren (`Capability → Procedure → Control → Evidence`). +Sie muss nur **anders gerendert** werden. + +## Entscheidung + +1. **Ein Implementation Playbook ist ein RENDERER über einer Capability** (`compliance/playbook`), + kein neuer Datenpfad. Es setzt die Reise zusammen aus: kuratiertem Playbook-Wissen + der + regulatorischen **Leverage** (welche Regelwerke eine umgesetzte Capability schließt, aus der + Optimization) + **injizierten** Procedure/Control/Evidence-Links (Execution-owned). + +2. **Playbook ≠ regulatorische Domäne (bewusste Unterscheidung):** + - Ein **Playbook** ist BreakPilots EIGENE Wissensschicht (`Capability → empfohlene Vorgehensweise + → Tools → Prozess → typische Nachweise → Controls`). Es führt KEIN neues Regelwerk ein. + - Eine **regulatorische Domäne** (z. B. ISO 14001 → Umweltrecht) ist neues *regulatorisches* + Wissen (Obligations, Anwendbarkeit), Eigentum von Legal Knowledge / Execution. + Beide skalieren unabhängig — und jede neue Domäne dockt sofort an denselben Playbook-Mechanismus an. + +3. **Der Engpass ist INHALT, nicht Software.** Der Renderer ist klein und fertig; der Wert wächst mit + Zahl und Tiefe der Playbooks. Eine Capability ohne Playbook rendert als `status: missing`-Stub — + das ehrliche „Content owed"-Signal. Playbook-Inhalt ist Reasoning-**Knowledge-Acquisition** + (wie `knowledge/transition_patterns/`): KI liefert den Erstentwurf, BreakPilot reviewt/versioniert/besitzt. + +## Konsequenzen + +- **Aus Diagnose wird Beratung:** derselbe Capability-Strang, der Auditor-Fragen (Interview) und + GF-Maßnahmen (Roadmap) liefert, liefert nun auch die Umsetzungsreise (Playbook). Konsistent mit + ADR-003 (ein Modell, viele Renderer). +- **Reifegrad + Ehrlichkeit:** Playbook-Inhalt ist `draft → reviewed → validated → proven`, ein + **Experten-Entwurf, KEINE normative Anforderung**; Tools/Schritte sind Empfehlungen. Controls + werden aus Execution injiziert — keine Execution-Daten im Reasoning-Produktcode. +- **Freeze-konform:** kein neues Metamodell, kein neuer Graph, kein neuer Corpus — `Playbook` ist + eine abgeleitete Sicht (computed-not-stored). Abhängigkeit `playbook → optimization → + transition_reasoning` ist azyklisch; die Capability Delta Engine bleibt hermetisch. +- **Priorisierung:** Playbooks kommen VOR neuen Domänen (ISO 14001), damit jede künftige Capability + sofort eine Umsetzungsreise bekommt. Content-Backlog = höchster Hebel zuerst. +- Diese ADR ist non-runtime → kein Deploy (siehe [ADR-001](ADR-001-runtime-deploy-policy.md)). diff --git a/docs-src/architecture/adr/ADR-005-knowledge-production-pipeline.md b/docs-src/architecture/adr/ADR-005-knowledge-production-pipeline.md new file mode 100644 index 00000000..33ee9acf --- /dev/null +++ b/docs-src/architecture/adr/ADR-005-knowledge-production-pipeline.md @@ -0,0 +1,55 @@ +# ADR-005: Knowledge Production — prepare deterministically, then curate + +- **Status:** Accepted +- **Datum:** 2026-06-27 +- **Typ:** Architektur-Entscheidung +- **Bezug:** [ADR-004](ADR-004-implementation-playbooks.md), [ADR-002](ADR-002-transition-is-data-not-architecture.md), Architektur-Freeze v1.0, [[transition-reasoning]], [[iace-quality-architecture]] + +## Kontext + +Mit Capability Delta, Optimization und Playbooks ist die Diagnose weitgehend fertig. Der nächste +Engpass ist NICHT „Content" (mehr Playbooks schreiben), sondern **Wissensproduktion**: würde man +200 Playbooks (und je Domäne neue Patterns/Reference-Szenarien) von Hand schreiben, verlagerte sich +der Engpass dauerhaft vom Engineering auf manuelle Wissenspflege. + +Der entscheidende Grundsatz war: **„Die Engine ändert sich nicht. Der Corpus wächst."** Diese ADR +ergänzt ihn: + +> **„Und der Corpus wird nicht manuell geschrieben. Er wird deterministisch vorbereitet und +> anschließend fachlich kuratiert."** + +## Entscheidung + +1. **BreakPilot produziert künftig keine fertigen Wissensartefakte, sondern ENTWÜRFE.** Ein Draft + Generator strukturiert deterministisch aus Daten, die die Software bereits besitzt + (Capability, Transition Pattern, Controls, Evidence, Regulatory Map / Leverage), einen Entwurf — + und überlässt das Fachwissen der menschlichen Kuratierung. + +2. **Spiegelung der Legal-Pipeline.** Wie `Gesetz → Parser → Obligation → Review` gilt jetzt + `neue Capability → Registry → Transition Pattern → Playbook Draft Generator → Expert Review → + versioniertes Playbook`. Dieselbe Logik für jedes Wissensartefakt (Playbooks, später Transition + Patterns, Reference-Szenarien). + +3. **Deterministisch-first (kein LLM im Kern).** Der Generator assembliert nur, was die Software + besitzt; weiche Felder (Tools / Prozessschritte / „wie machen das andere") werden als **TODO** + ausgewiesen. Optionale Modell-Anreicherung bleibt **offline, advisory, propose-only** — nie im + deterministischen Kern (vgl. [[iace-quality-architecture]]). + +4. **Freigabestatus.** Jedes Artefakt trägt einen Lifecycle + `draft_generated → in_review → reviewed → validated → proven` plus **Provenance je Feld** + (woraus es assembliert wurde) — Voraussetzung für Review-Workflow und Versionierung. + +## Konsequenzen + +- **Review statt Schreiben:** der Experte reviewt N Entwürfe statt N Artefakte zu schreiben — der + manuelle Aufwand sinkt massiv, ohne fachliche Kontrolle aufzugeben. +- **Neue Domänen werden billig:** sobald ein Domänen-Corpus (z. B. Umwelt) existiert, erzeugt + derselbe Generator erste Entwürfe — ISO 14001 wird ein Draft-+-Review-Problem, kein Schreibprojekt. +- **Internes Werkzeug:** die wertvollste Maschine ist nicht nur das Kunden-OS, sondern die + **Produktionsmaschine für das eigene regulatorische Wissen** — sie wird mit jeder Domäne wertvoller. +- **Freeze-konform:** kein neues Metamodell, kein Graph, kein neuer Corpus. `compliance/knowledge_production` + ist eine reine, deterministische Vorbereitung (computed-not-stored); Execution-Controls werden injiziert. +- **Phase A (Wissensproduktion) VOR Phase B (neue Domänen):** Draft-Generatoren (Playbook ✓, dann + Transition-Pattern, Reference-Szenario) + Review-Workflow + Versionierung + Freigabestatus, dann + ISO 14001 / IATF 16949 / IEC 62443. +- Diese ADR ist non-runtime → kein Deploy (siehe [ADR-001](ADR-001-runtime-deploy-policy.md)). diff --git a/docs-src/architecture/adr/ADR-006-knowledge-intake.md b/docs-src/architecture/adr/ADR-006-knowledge-intake.md new file mode 100644 index 00000000..48a9d0f6 --- /dev/null +++ b/docs-src/architecture/adr/ADR-006-knowledge-intake.md @@ -0,0 +1,54 @@ +# ADR-006: Knowledge Intake — classify and assess impact before extraction + +- **Status:** Accepted +- **Datum:** 2026-06-27 +- **Typ:** Architektur-Entscheidung +- **Bezug:** [ADR-005](ADR-005-knowledge-production-pipeline.md), [ADR-002](ADR-002-transition-is-data-not-architecture.md), Architektur-Freeze v1.0, [[transition-reasoning]], [[iace-quality-architecture]] + +## Kontext + +Vier Produktionspipelines folgen demselben Muster `Rohwissen → deterministischer Entwurf → +Expertenreview → veröffentlichter Wissensbaustein` (Obligations, Capabilities, Playbook-Drafts, +Reference-Szenarien). Aber **woher kommt das Rohwissen, und wie verarbeitet man es effizient?** + +Heute beginnt die Pipeline beim Parser (`PDF → Parser → Review`). Damit startet man bei JEDEM neuen +Dokument wieder bei Null. Der eigentliche Aufwand der Wissensproduktion ist nicht das Schreiben, +sondern das **gezielte Aktualisieren**: wenn morgen 20 Dokumente erscheinen — welche 15 kann man +ignorieren und welche 5 verändern tatsächlich den Wissensbestand? + +## Entscheidung + +1. **Vor dem Parser steht eine neue Stufe: Knowledge Intake.** Sie extrahiert KEINEN Inhalt, sondern + ordnet ein neues Dokument nur ein (Klassifikation) und bestimmt seinen **Impact** auf den + bestehenden Wissensbestand. + +2. **Output ist ein `KnowledgePackage` (Impact-Analyse), kein Inhalt:** welche bestehenden + Capabilities / Playbooks / Transition Patterns / Reference-Szenarien / (injizierten) Obligations + das Dokument wahrscheinlich betrifft, ob es eine **neue Domäne** ist, und ein Triage-Level + (`HIGH / LOW / NONE / NEW_DOMAIN`) mit Empfehlung. + +3. **Deterministisch, kein LLM.** Intake schneidet die Dokument-Signale (deklarierte Regelwerke + + Stichworte) gegen einen Index des vorhandenen Wissens. Optionale Modell-Anreicherung bleibt + offline/advisory (vgl. [[iace-quality-architecture]]). + +4. **Vollständige Wissensfabrik:** + `Knowledge Intake → Knowledge Package → Parser → Draft Generator → Expert Review → + Published Knowledge → Reference Suite`. + +## Konsequenzen + +- **Targeted Updating statt Schreiben:** statt „hier sind 200 Seiten PDF" liefert das System eine + Impact-Analyse („betrifft 4 Capabilities, 2 Playbooks, RTS-003; keine neue Domäne"). Das spart + enorm viel Review-Aufwand und ist die eigentliche Knowledge Production. +- **Neue Quellen werden automatisch eingeordnet:** CRA-FAQ, MaschinenVO-Guidance, ENISA-Empfehlung, + BSI-Orientierungshilfe → je eine Impact-Analyse statt Rohtext. +- **Geänderte Phasen-Reihenfolge:** **A1 Knowledge Intake** (klassifizieren + Impact + Knowledge + Package) → **A2 Draft Production** (Transition Patterns / Playbooks / Reference-Szenarien) → + **A3 Expert Review** (Review / Versionierung / Veröffentlichung). Erst danach Phase B (neue Domänen). +- **Freeze-konform:** kein neues Metamodell, kein Graph, kein neuer Corpus. `compliance/knowledge_intake` + ist eine reine, deterministische Sicht (computed-not-stored); Obligations werden injiziert. Bekannte + Verfeinerung: Regelwerk-ID-Normalisierung (CRA ↔ Cyber Resilience Act) — vom Intake ehrlich sichtbar. +- **Strategische Bedeutung:** die Plattform wird von einem Compliance-Produkt zu einer **kontinuierlich + lernenden regulatorischen Wissensbasis** — und Intake ist der Filter, der bestimmt, was überhaupt + Arbeit auslöst. +- Diese ADR ist non-runtime → kein Deploy (siehe [ADR-001](ADR-001-runtime-deploy-policy.md)). diff --git a/docs-src/architecture/adr/ADR-007-regulatory-completeness.md b/docs-src/architecture/adr/ADR-007-regulatory-completeness.md new file mode 100644 index 00000000..06b83310 --- /dev/null +++ b/docs-src/architecture/adr/ADR-007-regulatory-completeness.md @@ -0,0 +1,53 @@ +# ADR-007: Regulatory Completeness — auditable knowledge coverage, not confidence + +- **Status:** Accepted +- **Datum:** 2026-06-27 +- **Typ:** Architektur-Entscheidung +- **Bezug:** [ADR-006](ADR-006-knowledge-intake.md), [ADR-003](ADR-003-capability-delta-engine-with-renderers.md), Architektur-Freeze v1.0, [[transition-reasoning]], [[reasoning-vs-compliance-boundary]] + +## Kontext + +Die Engine beantwortet inzwischen *Was gilt? · Was fehlt? · Wie umsetzen?*. Es fehlt eine +übergeordnete Fähigkeit: **„Wie sicher sind wir, dass diese Antwort VOLLSTÄNDIG ist?"** Das ist +NICHT Confidence (Vertrauen in eine einzelne Aussage), sondern Abdeckung (welche Teile des Problems +haben wir überhaupt bewertet). + +Der Übergang von Feature- zu Produktentwicklung verlangt, dass der Kunde — gerade in regulierten +Branchen — sehen kann, was die Plattform NICHT weiß und warum sie dazu keine Aussage trifft. + +## Entscheidung + +1. **Eine Regulatory Completeness Engine** (`compliance/completeness`, interne Qualitätsmaschine, + non-runtime) trennt für eine Beratung **IDENTIFIZIERT** (getriggerte Regelwerke) von **BEWERTET** + (validierter Korpus UND geklärte Anwendbarkeit) und **begründet jede Lücke**. + +2. **Zwei Arten „offen", je mit Begründung:** + - **Korpus-Lücke** — kein validierter Korpus (z. B. Environmental) → `future_corpus`. + - **Anwendbarkeits-Unsicherheit** — Korpus vorhanden, aber Anwendbarkeit unklar (Data Act, + `generates_usage_data` unbekannt) → `query_required` mit deciding question. + +3. **Die Metrik sind ZÄHLUNGEN, keine einzelne Prozentzahl.** Nicht „87 %", sondern + `Identifiziert N · bewertet M · offen K · Unsicherheiten U · Begründung ja`. Plus ein ehrlicher + **Audit-Satz**: „Wir bewerteten M von N Domänen; K sind nicht im validierten Korpus / anwendungs- + unsicher und wurden bewusst nicht bewertet." + +4. **Annahmen und begründete Ausschlüsse sind explizit** (z. B. „kein Funkmodul", + „keine personenbezogenen Nutzungsdaten"). + +## Konsequenzen + +- **Auditierbar + ehrlich:** das System behauptet KEINE Vollständigkeit, es macht den eigenen + Wissensstand transparent. Direkte Fortsetzung der Welt-1-Disziplin ([[reasoning-vs-compliance-boundary]]) + auf Produktebene. +- **Fortschritt je Domäne wird automatisch dokumentiert:** landet der Umwelt-Korpus (ISO 14001), + kippt `Environmental` von `unsupported`/offen auf `validated`/bewertet — die Engine zeigt, WARUM + sich die Antwort verändert hat. +- **Verkaufsargument:** „Wir sagen Ihnen nicht nur, was wir wissen — wir zeigen transparent, was wir + noch nicht wissen und warum wir dazu keine Aussage treffen." Transparenz = Vertrauen. +- **Freeze-konform:** kein neues Metamodell, kein Graph, kein neuer Corpus. `compliance/completeness` + ist eine reine, deterministische Aggregation (computed-not-stored); Corpus-Status + Obligation-Zahl + werden injiziert (Execution-/Kuratoren-owned). +- **Phasen-Reihenfolge:** **A Wissensfabrik** (Intake ✓ / Draft ✓ / Review) → **A½ Regulatory + Completeness** (diese ADR) → **B neue Domänen** (ISO 14001 / REACH / CLP / IATF 16949 / IEC 62443). + Completeness VOR den Domänen, damit jeder Domänen-Zuwachs sofort messbar wird. +- Diese ADR ist non-runtime → kein Deploy (siehe [ADR-001](ADR-001-runtime-deploy-policy.md)). diff --git a/docs-src/architecture/adr/ADR-008-from-architecture-to-domains.md b/docs-src/architecture/adr/ADR-008-from-architecture-to-domains.md new file mode 100644 index 00000000..57326ae9 --- /dev/null +++ b/docs-src/architecture/adr/ADR-008-from-architecture-to-domains.md @@ -0,0 +1,52 @@ +# ADR-008: From architecture to domains — Phase B as law-first Knowledge Programs + +- **Status:** Accepted +- **Datum:** 2026-06-27 +- **Typ:** Architektur- / Strategie-Entscheidung +- **Bezug:** [ADR-005](ADR-005-knowledge-production-pipeline.md), [ADR-006](ADR-006-knowledge-intake.md), [ADR-007](ADR-007-regulatory-completeness.md), [ADR-002](ADR-002-transition-is-data-not-architecture.md), Architektur-Freeze v1.0, [[regulatory-intelligence-vision]] + +## Kontext + +Die Plattform ist außergewöhnlich vollständig: Produkt-/Company-Profil, Scope, Regulatory Map, +Interpretation, Capability Registry, Capability Delta, Optimization, Playbooks, Knowledge Intake, +Knowledge Production, Regulatory Completeness, Reference Scenarios. **Der Engpass ist nicht mehr +Software, sondern: „Ist das Wissen fachlich richtig und vollständig?"** + +Damit endet die Plattformentwicklung und die eigentliche Wissensentwicklung beginnt. Der +Wettbewerbsvorteil entsteht ab jetzt aus Qualität und Tiefe des Korpus, nicht aus mehr Software. + +## Entscheidung + +1. **Die Runtime-Architektur wird bewusst stabil gehalten. Kein weiteres Runtime-Framework.** + Phase B fügt KEINE Architektur hinzu — sie fügt **Domänen** (Daten + Wissen) hinzu. + +2. **Phase B wird in Domain Knowledge Programs organisiert, nicht in einzelne Regelwerk-Features.** + Jedes Programm liefert dieselbe Produktionsstraße: + `Regulatory Corpus → Obligations → Capabilities → Transition Patterns → Playbooks → + Reference Scenarios → Completeness`. + Geplant: Environmental · Automotive · OT/IEC 62443 · Functional Safety. + +3. **Law-first.** Ein Managementsystem (z. B. ISO 14001) ist NICHT die Domäne, sondern ein möglicher + Quellzustand. Reihenfolge: **Recht → Obligations → Capabilities → (Managementsystem) → Delta.** + Der Kunde fragt „welche Anforderungen gelten für mein Produkt?", nicht „wie komme ich von ISO 14001 weg?". + Das Quellzustand→Korpus-Transition-Pattern entsteht ZULETZT, wenn beide Seiten bekannt sind. + +4. **Erstes Programm: Environmental Knowledge Program** (`knowledge/programs/environmental.yaml`), + als vollständige Domäne mit denselben Artefakten wie CRA/MaschinenVO. Stufen: + **B1 Environmental Regulatory Corpus** (Wasser/Chemikalien/Emissionen/Energie/Abfall/ + Produktverantwortung — nur Recht + Pflichten) → **B2 Environmental Capability Model** → + **B3 Transition Patterns (ISO 14001 → Korpus)**. + +## Konsequenzen + +- **Programme statt Features:** jedes Programm ist eine maschinenlesbare Definition (`programs/*.yaml`), + deren Stufen-Status kippt, wenn Artefakte landen. Die Reference Suite + Completeness dokumentieren + den Fortschritt je Domäne automatisch (Environmental-Zelle `unsupported → validated`). +- **Ownership-Handoffs (Board):** **B1 → Legal Knowledge / Obligation Registry**, + **B2 → Compliance Execution** (Capability Registry wächst), **B3 + Playbooks + Reference Scenarios → + Reasoning**. Keine Session baut die Artefakte einer anderen; Koordination über das Board. +- **Jede neue Domäne erweitert AUTOMATISCH** Scope, Gap, Capability Delta, Optimization, Playbooks, + Reference Scenarios und Completeness — **ohne Änderung der Runtime-Architektur**. Genau das Ziel, + das der Freeze v1.0 erreichen sollte. +- **Reifegrad:** bis hierher wurde Architektur gebaut; ab jetzt werden Domänen gebaut. +- Diese ADR ist non-runtime → kein Deploy (siehe [ADR-001](ADR-001-runtime-deploy-policy.md)). diff --git a/docs-src/architecture/adr/ADR-009-domain-knowledge-program.md b/docs-src/architecture/adr/ADR-009-domain-knowledge-program.md new file mode 100644 index 00000000..f07c00f0 --- /dev/null +++ b/docs-src/architecture/adr/ADR-009-domain-knowledge-program.md @@ -0,0 +1,53 @@ +# ADR-009: Domain Knowledge Program — one 7-stage production line per domain + +- **Status:** Accepted +- **Datum:** 2026-06-27 +- **Typ:** Architektur- / Organisations-Entscheidung +- **Bezug:** [ADR-008](ADR-008-from-architecture-to-domains.md), [ADR-007](ADR-007-regulatory-completeness.md), [ADR-005](ADR-005-knowledge-production-pipeline.md), Architektur-Freeze v1.0, [[company-intelligence-2a]] + +## Kontext + +Der Engpass ist nicht mehr Architektur, Controls oder „Wissen" allgemein, sondern präzise: +**Domänenmodellierung.** Phase B (ADR-008) wird daher nicht als Einzel-Regelwerk-Features +organisiert, sondern als EIN Arbeitsprogramm mit Unterprogrammen je Domäne — alle durch dieselbe +Produktionsstraße. Kein weiteres Architektur-Epic, keine neue Runtime-Architektur. + +## Entscheidung + +1. **Einstieg über die INDUSTRIE, nicht über das Regelwerk.** Der Kunde sagt „ich baue + Verpackungsmaschinen / bin Automobilzulieferer / baue Parksysteme", nicht „erklär mir ISO 9001". + Die Pipeline beginnt davor: `Industry → Domain Model → Requirement Sources → Requirements → + Capabilities → … → Completeness`. + +2. **Eine 7-Stufen-Checkliste, identisch für JEDE Domäne:** + 1 Domain Model · 2 Requirement Sources · 3 Capability Registry · 4 Transition Patterns · + 5 Playbooks · 6 Reference Scenarios · 7 Completeness. Ownership je Stufe (1 Reasoning · 2 Legal-KG · + 3 Execution · 4–7 Reasoning). Das ist der Skalierungsmechanismus: jede neue Domäne nutzt dieselbe + Straße, die bestehenden Engines erweitern sich automatisch. + +3. **Domänen tragen `typical_requirement_sources` + `typical_certifications` → Pre-Onboarding-HYPOTHESE + (ETO-Einsicht).** Vor dem Onboarding: „diese Prozesswelt ist *wahrscheinlich* vorhanden" — als + Hypothese, nie Wahrheit. Speist Company 2A als `inferred`, nie `confirmed`. Wir wollen nicht wissen, + OB ein Automobilzulieferer ISO 9001 hat (das hat jeder), sondern welche Fähigkeiten dadurch + wahrscheinlich schon vorhanden sind. + +4. **Per-Domain-KPI, reproduzierbar (computed-not-stored).** Reifegrad wird aus dem ECHTEN Korpus + abgeleitet (modellierte Sources / Transition Patterns / Playbooks / Reference Scenarios / bewusst + ausgewiesene Lücken — auf Basis der Regulatory Completeness Engine), NICHT als kuratierte Zahl. + Programm-Shells speichern KEINEN Stufen-Status. Keine Marketingzahl. + +5. **Domain Knowledge Program v1 — Backlog nach Kundennutzen** (getrennt vom KPI nach Korpusstand): + 1 Industrial Automation · 2 Environmental · 3 Automotive · 4 Medical · 5 Energy. + +## Konsequenzen + +- **Programme statt Features:** jede Domäne ist eine maschinenlesbare Definition (`programs/*.yaml`); + der Reifegrad-KPI im Reference-Suite ist aus dem Korpus abgeleitet und differenziert ehrlich + (Industrial Automation führt, Environmental 0 % — die Arbeit liegt vor uns). +- **Backlog ≠ KPI:** der Backlog ordnet nach Kundennutzen, der KPI misst den echten Korpusstand — + bewusst getrennt (z. B. eine Domäne kann hoch im Backlog, aber niedrig im KPI stehen). +- **Arbeit verschiebt sich endgültig von Software- zu Wissensproduktion.** Wettbewerbsvorteil = + Qualität und Breite der modellierten Domänen. +- **Freeze-konform:** kein neues Metamodell, kein Graph, kein neues `compliance/`-Modul. Nur + Programm-Daten (`knowledge/programs/`) + abgeleitete Reporting-Sicht im Reference-Suite. +- Diese ADR ist non-runtime → kein Deploy (siehe [ADR-001](ADR-001-runtime-deploy-policy.md)). diff --git a/docs-src/architecture/adr/ADR-010-operational-knowledge-transition-unit.md b/docs-src/architecture/adr/ADR-010-operational-knowledge-transition-unit.md new file mode 100644 index 00000000..95ab7861 --- /dev/null +++ b/docs-src/architecture/adr/ADR-010-operational-knowledge-transition-unit.md @@ -0,0 +1,50 @@ +# ADR-010: Operational Knowledge — the transition is the unit of knowledge + +- **Status:** Accepted +- **Datum:** 2026-06-27 +- **Typ:** Produkt- / Wissens-Strategie-Entscheidung +- **Bezug:** [ADR-009](ADR-009-domain-knowledge-program.md), [ADR-002](ADR-002-transition-is-data-not-architecture.md), [[transition-reasoning]], [[strategy-requirements-intelligence]] + +## Kontext + +Aus allen Kundengesprächen ergibt sich dieselbe Frage: nicht „was steht im CRA?", sondern +**„ich habe heute ISO 27001 und TISAX — welche drei Dinge fehlen mir noch für den CRA?"**. Die +verkaufte Einheit ist damit weder das Gesetz noch die Capability, sondern die **Transition** +(`Ausgangszustand → Zielzustand`). Den Domänen-Backlog nach „jetzt EMV modellieren" zu ordnen, +verfehlt das — niemand kauft eine „EMV-Domain", Kunden kaufen „ISO 9001 → CRA". + +## Entscheidung + +1. **Die Wissenseinheit ist die TRANSITION.** Der Operational-Knowledge-Backlog + (`knowledge/programs/transitions.yaml`) listet die ~20–30 tatsächlich nachgefragten Transitionen + (von ~N·(N−1) theoretisch möglichen) mit Priorität. Das ist der eigentliche Verkaufs-Backlog. + +2. **Drei Wissensebenen, die ineinandergreifen:** + `Regulatory Knowledge` (Gesetze/Normen/Leitlinien) → **`Operational Knowledge`** (Transition + Patterns · Playbooks · Capability-Deltas) → `Verification Knowledge` (Code · SBOM · Doku · + Architektur · Prozesse; = Vision V2 / Requirements Verification). Die **mittlere Ebene ist der + größte Differenzierer**: sie beantwortet nicht nur *was* gefordert ist, sondern *wie ein Unternehmen + mit seinem heutigen Reifegrad dorthin kommt*. + +3. **Zweiter, stärkerer KPI: Transition Coverage.** Pro Top-Transition ein Status, DERIVED aus dem + Transition-Pattern-Korpus (`reviewed/validated/proven → ✅`, `draft → 🟡`, `none → ⚪`), + computed-not-stored. „ISO 9001 → MaschinenVO ⭐⭐⭐⭐⭐ aber ⚪" ist ein viel stärkerer Produktindikator + als „EMV ist zu 30 % modelliert". + +4. **Eine Domäne ist ein TRANSITION PROGRAM mit zwei parallelen Tracks:** + - **Track A (Breite):** verbleibende Requirement Sources der Domäne modellieren (EMV, RED, + IEC 62443, NIS2 …) — Korpus wächst (Stufe 2–3, @Legal-KG/@Execution). + - **Track B (Produkt):** für jede neu modellierte Source sofort die wichtigsten Transition Patterns + + Playbooks + Reference Scenarios erzeugen — verkaufbares/einsetzbares Wissen (Stufe 4–6, @Reasoning). + +## Konsequenzen + +- **Backlog ist transition-getrieben, nicht regelwerk-getrieben.** Die höchstpriorisierte Lücke + (z. B. `ISO 9001 → MaschinenVO`, ⭐⭐⭐⭐⭐, ⚪) ist die nächste Track-B-Arbeit — nicht „EMV". +- **Task #49 wird zum „Industrial Automation Transition Program"** (Track A + Track B parallel), sodass + wachsendes Domänenwissen unmittelbar in Produktwert übersetzt wird. +- **Operational Knowledge = der Moat.** Regulatory ist Commodity (jeder kann Gesetze lesen), + Verification ist Vision V2; die Transition dazwischen ist der differenzierende Asset. +- **Freeze-konform:** kein neues Metamodell/Graph/Modul. Nur Daten (`transitions.yaml`) + eine + abgeleitete Reporting-Sicht; Status aus dem vorhandenen Pattern-Korpus berechnet. +- Diese ADR ist non-runtime → kein Deploy (siehe [ADR-001](ADR-001-runtime-deploy-policy.md)). diff --git a/docs-src/architecture/adr/ADR-011-journey-matcher-delta-explains-journey.md b/docs-src/architecture/adr/ADR-011-journey-matcher-delta-explains-journey.md new file mode 100644 index 00000000..3e38ac05 --- /dev/null +++ b/docs-src/architecture/adr/ADR-011-journey-matcher-delta-explains-journey.md @@ -0,0 +1,54 @@ +# ADR-011: Journey Matcher — the delta explains the journey (Delta -> Journey) + +- **Status:** Accepted +- **Datum:** 2026-06-28 +- **Typ:** Architektur-Entscheidung (neues Modul — vom User ausdrücklich freigegeben) +- **Bezug:** [ADR-003](ADR-003-capability-delta-engine-with-renderers.md), [ADR-002](ADR-002-transition-is-data-not-architecture.md), [[transition-reasoning]], [[strategy-requirements-intelligence]], journey-model-spec-v1 + +## Kontext + +Bisher war die **Journey die Ursache**: der Berater wählte vorne eine Journey (`Goal → Journey → Delta`). +Genau das war der eine offene „Sprung" aus Customer Mission #1 (Scope → Journey). Wir haben ihn bewusst +NICHT zu früh gebaut, sondern erst **fünf bewusst unterschiedliche Zielarten** validiert (Mission #1–#5: +Regulation · Certification · Public Tender · OEM-Spec · Umwelt/Material). Damit existiert die Diversität, +um aus **beobachteten Fällen** zu generalisieren statt aus Annahmen — die Voraussetzung, den Selektor +überhaupt sinnvoll zu bauen. + +## Entscheidung + +1. **Umkehrung der Reihenfolge: `Goal → Required → Delta → Journey`.** Die Journey ist die **Erklärung** + des Capability Deltas, nicht seine Ursache. Genannt **Journey Matcher / Explainer**, nicht „Selector". + +2. **Neues Modul `compliance/journey_matcher/` = die dritte Funktion `Delta → Journey`** — neben + Company 2A (`Evidence → Capability`) und RS-005 (`Capability → Delta`). Drei **unabhängige, + austauschbare** Funktionen, drei verschiedene Probleme. + +3. **Der Matcher sieht NUR das Capability Delta** — keine Zertifikate, kein Regelwerk, kein Ziel. + Journey-Signaturen sind **zertifikat-agnostische Capability-Cluster** (`Input Pattern → Output Pattern`); + die IDs wie „ISO27001 → CRA" sind nur eine Beschreibung der Cluster, der Matcher liest sie nie. + **Score = Anteil des Deltas, den eine Journey erklärt** (Recall über die fehlenden Capabilities), nie + ein „Fit" oder ein Konformitätsurteil. `journey_only` dokumentiert, wo eine Journey über das Delta + hinausreicht (eine breite Journey wird nicht still bevorzugt). + +4. **Bewusst dumm + deterministisch:** reine Mengenüberlappung, **kein ML, keine Embeddings, kein LLM**. + Voll auditierbar (matched / unexplained / journey_only / Kontext-Signale). Ein lernendes Ranking kann + später DAVOR gesetzt werden; der deterministische Kern bleibt nachvollziehbar. Kontext (Branche/ + Produkttyp/Zielart) ist nur **dokumentierte Korroboration + Tie-Break**, nie Teil des Scores. + +5. **Signaturen werden INJIZIERT**, nicht im Kern geladen — die Engine bleibt hermetisch (wie RS-005). + +## Konsequenzen + +- **Der „Scope → Journey"-Sprung aus Mission #1 ist aufgelöst:** es gibt keinen Journey-Matcher als + Sonderlogik je Zielart — die Journey ergibt sich aus dem Delta. An echten Pattern validiert: ein + CRA+MaschinenVO-Delta rankt die Konvergenz-Journey 100 %, „ISO27001 → CRA" 56 % (verfehlt die + Maschinensicherheit), „ISMS → TISAX" 0 %. +- **Letzter wirklich architektureller Baustein.** Alles danach ist überwiegend **Wissensarbeit, + Korpusaufbau, Domänenmodellierung** — in der Pipeline `Reality → Evidence → Capability Profile → + Required State → Capability Delta → Journey → Roadmap → Playbooks → Verification` kommt „Regulation" + nicht mehr als Sonderfall vor. +- **Freeze-Ausnahme, bewusst:** der User hat dieses EINE neue Modul ausdrücklich freigegeben. Kein + weiteres Metamodell/Graph. Non-runtime (kein App-Caller) → kein Deploy ([ADR-001](ADR-001-runtime-deploy-policy.md)). +- **Folgearbeit (nicht jetzt):** Journeys als reine `Capability Cluster A → Cluster B` (statt ISO/CRA-IDs); + `Intent → Scope → Journey`-Ebene darüber; lernendes Ranking als Vor-Stufe; `relevance(evidence, target)` + als eigene Berechnung (aus Mission #5). diff --git a/docs-src/architecture/adr/ADR-012-smart-onboarding-advisor.md b/docs-src/architecture/adr/ADR-012-smart-onboarding-advisor.md new file mode 100644 index 00000000..1c5aeaab --- /dev/null +++ b/docs-src/architecture/adr/ADR-012-smart-onboarding-advisor.md @@ -0,0 +1,45 @@ +# ADR-012: Smart Onboarding Advisor — make the knowledge usable in onboarding (orchestration) + +- **Status:** Accepted +- **Datum:** 2026-06-28 +- **Typ:** Architektur-Entscheidung (Runtime-Schritt — Orchestrierung, KEINE neue Engine) +- **Bezug:** [ADR-003](ADR-003-capability-delta-engine-with-renderers.md), [ADR-011](ADR-011-journey-matcher-delta-explains-journey.md), [[strategy-knowledge-layers-and-hypotheses]], [[evidence-attributed-to-origin]], [[transition-reasoning]] + +## Kontext + +Das Wissen ist gebaut; der nächste Schritt ist, es **automatisch im Onboarding** nutzbar zu machen — der +Vertrieb ist nicht schulbar und darf KEINE Regelwerke auswählen oder interpretieren. Zugleich gilt die +reale Grenze: **proprietäre Normen (ISO/TISAX/PCI…) dürfen nicht ingestiert werden** — also wird aus den +Zertifikaten über eine **kuratierte Hypothese** (Welt-1) auf *wahrscheinlich vorhandene* Fähigkeiten +geschlossen, nie auf „erfüllt". + +## Entscheidung + +1. **`compliance/onboarding/` ist ein ORCHESTRATOR, keine neue Engine.** Er verdrahtet die bestehenden + Bausteine zu einem Flow: Company 2A (`Evidence→Capability`) → RS-005 (`Capability→Delta`) → + Optimization (`Delta→Roadmap`) → Completeness. Keine neue Reasoning-Engine, Capability-Registry oder + Metamodell (Freeze). +2. **`advisor_start(input, cert_hypotheses, target_requirements, …)`** liefert: `inferred_assumptions`, + `rejected_assumptions`, `next_best_questions` (≤5), `capability_delta`, `top_measures`, + `evidence_requests`, `unsupported_domains`, `completeness_summary`. +3. **Welt-1-Disziplin:** Zertifikate **reduzieren Fragen, erfüllen aber NICHTS automatisch** + (`verification_required`). **`relevance(evidence, target)`** hält ISO 14001 aus dem CRA-Ergebnis heraus + (nicht-relevante Zertifikate → `rejected_assumptions`, Grund „relevance = 0"). +4. **Nur die nächsten besten Fragen** (≤5), deterministisch gerankt nach + `information_gain + leverage (regulatory+business) + unknown_high_risk + evidence_missing`; **jede Frage + erklärt sich** (`why`). **Jede Antwort aktualisiert das Profil** (`apply_answer` → Delta schrumpft). +5. **Zertifikat→Capability-Hypothesen und Ziel-Anforderungen werden INJIZIERT** — kuratiertes Wissen, + NICHT im Code ([[evidence-attributed-to-origin]]). Die Hypothesen-Kuratierung ist ein eigener, + ausgelagerter Knowledge-Task. + +## Konsequenzen + +- **Erster „App-Caller", der die Engines zu einem Produkt-Flow verbindet** — der vom User benannte + „richtige nächste Runtime-Schritt". Noch OHNE Endpoint/DB-Persistenz → aktuell **kein Runtime-Effekt → + kein Deploy** ([ADR-001](ADR-001-runtime-deploy-policy.md)); Deploy, sobald `POST /onboarding/advisor-start` + + Persistenz + Frontend verdrahtet sind (Folgeschritt). +- **7 Akzeptanzkriterien erfüllt + getestet** (Multi-Cert · ISO 14001 nicht fälschlich relevant · + Welt-1 · ≤5 Fragen · Frage erklärt sich · Antwort updatet Profil · Vertrieb interpretiert nichts). +- **Langfristiger Moat = EMPIRIE:** `confidence` der Hypothesen kommt später aus BEOBACHTUNGEN + (bestätigt/widerlegt je Kunde), nicht vom LLM — drei Wissensebenen + ([[strategy-knowledge-layers-and-hypotheses]]). diff --git a/docs-src/architecture/domain-vocabulary-spec-v1.md b/docs-src/architecture/domain-vocabulary-spec-v1.md new file mode 100644 index 00000000..d89c14b1 --- /dev/null +++ b/docs-src/architecture/domain-vocabulary-spec-v1.md @@ -0,0 +1,76 @@ +# Domain Vocabulary — specification (PROPOSAL v1) + +- **Status:** PROPOSAL / draft. Beantwortet EINE Frage, bevor die nächste Journey entsteht. KEIN + Runtime-Modul, KEIN Parser, KEINE neue Architektur. +- **Datum:** 2026-06-28 +- **Bezug:** [[master-capability-registry-2c]], [journey-model-spec-v1.md](journey-model-spec-v1.md), [ADR-010](adr/ADR-010-operational-knowledge-transition-unit.md), [[strategy-requirements-intelligence]] + +## 1. Problem: es fehlt die SPRACHE + +Wir haben fünf Wissensobjekte (Requirement · Capability · Journey · Playbook · Reference Scenario), +aber kein **Vokabular**. Heute heißt eine Transition `ISO9001 → MaschinenVO`. Dieselbe Reise könnte +auch `Quality Management → Product Safety`, `QMS → Machinery Compliance` oder `Operational Excellence +→ CE` heißen. Bei 40 Requirement Sources / 300 Capabilities / 150 Journeys / 500 Playbooks wird die +**Benennung selbst zum Problem** — und ohne Vokabular modellieren wir dieselbe fachliche Sache mehrfach +unter verschiedenen Namen (genau die Duplikation, die wir bei Controls/Capabilities vermieden haben). + +## 2. Die EINE Frage + +> **Welche Begriffe unseres Systems sind IDENTITÄTEN, und welche sind nur DARSTELLUNGEN derselben +> fachlichen Bedeutung?** + +## 3. Antwort: Identitäten vs. Darstellungen + +| Ebene | Identität (stabile ID) | Owner | Darstellungen (canonical name + aliases) | +|---|---|---|---| +| Requirement | `RQ-xxxxx` | Legal/Execution | „SBOM-Pflicht" / Art.-Refs | +| Capability | `MCAP-xxxxx` (Registry 2C) | **Execution** | „Patch Management" + [Software Updates, Security Updates, Update Management, Patch Process, Vulnerability Remediation, Security Patch Procedure] | +| **Requirement Source / Target** (Regelwerk/Norm) | `reg:cra`, `reg:iso9001` … | **shared (Legal/Execution), Reasoning seedet** | „Cyber Resilience Act" + [CRA, Reg. (EU) 2024/2847]; „ISO 9001" + [QMS, Quality Management System] | +| **Journey Class** | `MJRN-xxxxx` (**PROVISIONAL**) | **Reasoning** | „Quality Management → Product Compliance" | +| Journey (Instanz) | `(source-id → target-id)` | Reasoning | `iso9001 → maschinenvo` | +| Playbook | `MPLB-xxxxx` | Reasoning | „SBOM aufbauen" | + +**Identität = die Sache; Darstellung = jeder Name dafür.** Eine Journey-Instanz ist stabil, weil ihre +Endpunkte (Source/Target) IDENTITÄTEN sind, nicht Strings — egal ob man „ISO9001", „QMS" oder +„Operational Excellence" schreibt. + +## 4. Capability-Vokabular = Capability Registry (2C), NICHT neu bauen + +Canonical Name + Aliases einer Capability sind bereits ein **Registry-2C-Konzern** (Execution): die +Registry hat stabile `MCAP`-IDs + Relationstypen `equivalent`/`related` (= Synonyme) + Provenance. +Das Domain Vocabulary DUPLIZIERT das nicht — es macht es nur explizit und ergänzt zwei NEUE Ebenen, +die Reasoning besitzt: **Regelwerk-Identitäten** und **Journey Class**. + +## 5. Sofort-Nutzen: Regelwerk-Normalisierung (löst einen offenen TODO) + +`reg:cra` mit canonical „Cyber Resilience Act" + aliases `[CRA, Cyber Resilience Act, Reg. (EU) +2024/2847]` löst genau die **Regelwerk-ID-Normalisierung**, die Transition-Coverage-KPI + Knowledge +Intake bisher als TODO führen (CRA vs „Cyber Resilience Act"). `knowledge/vocabulary/regulations.yaml` +wird die SHARED Quelle; die Reference-Suite-KPIs lesen Aliase daraus statt aus hartkodierten Maps. + +## 6. Journey Class (PROVISIONAL — eigene Rule of Three) + +Eine Journey CLASS clustert Instanzen, die „dieselbe Reise" sind. `knowledge/vocabulary/journey_classes.yaml` +clustert unsere realen Transitionen — z. B. **`Information Security → Product Cybersecurity`** +(ISO27001→CRA, TISAX→CRA, IEC62443→CRA) und **`Quality Management → Product Compliance/Safety`** +(ISO9001→CRA, ISO9001→MaschinenVO, später ISO13485→MDR). So schreibt man NICHT für jede Zertifizierung +eine neue Journey. **Journey Class ist eine NEUE Abstraktion → provisional (kein MJRN-Mint), bis sie +sich selbst an ≥3 Instanzen je Klasse bewährt** ([[rule-of-three-canonicalization]]). + +## 7. Nebeneffekt: Requirements Intelligence (Vision V2) + +Wenn später ein Tender „Security Patch Procedure" fordert, erkennt BreakPilot den Alias von `MCAP-0017`, +ohne dass irgendwo „Patch Management" steht. Stabile Begriffe → konsistente Parser, Tender-Vergleiche, +Playbooks, Knowledge Intake. Das ist die Grundlage der Requirements Verification Platform +([[strategy-requirements-intelligence]]). + +## 8. Reihenfolge (User 2026-06-28) + +`Vocabulary` → `Transition #2` → `Transition #3` → Rule of Three → Journey kanonisch. Die nächsten zwei +Journeys zeigen, OB das Journey-MODELL stabil ist; das Vokabular zeigt, OB wir dieselbe fachliche Sache +immer GLEICH benennen — langfristig mindestens genauso wichtig. + +## 9. Was das NICHT ist + +- Kein Runtime/Parser/Engine, kein MCAP/MJRN-Minting (Freeze unberührt). Seed-Daten + Spec. +- Non-runtime → kein Deploy (ADR-001). diff --git a/docs-src/architecture/journey-model-spec-v1.md b/docs-src/architecture/journey-model-spec-v1.md new file mode 100644 index 00000000..397352ed --- /dev/null +++ b/docs-src/architecture/journey-model-spec-v1.md @@ -0,0 +1,149 @@ +# Journey Model — specification (ACCEPTED AS CONCEPT, pending canonicalization) + +- **Status:** **ACCEPTED AS CONCEPT, PENDING CANONICALIZATION** (User 2026-06-28, Rule of Three). + Journey ist ab sofort die bevorzugte **Denkweise**; das PERSISTIERTE Artefakt heißt vorerst weiter + `Transition Pattern`. **KEIN Rename, KEINE Migration, KEINE Runtime-Änderung.** Kanonisierung + (`Transition Pattern → Journey` + ADR-011) erst, NACHDEM sich die Journey an **≥ 3 unterschiedlichen + Transitionen** bewährt hat (siehe §9). Schützt vor zu früher Abstraktion — wie bei Master Controls/ + Capabilities: erst beweisen, dann kanonisieren. +- **Datum:** 2026-06-28 +- **Typ:** Konzeptionelles Datenmodell (KEIN Runtime-Modul, KEINE neue Architektur) +- **Bezug:** [ADR-003](adr/ADR-003-capability-delta-engine-with-renderers.md), [ADR-004](adr/ADR-004-implementation-playbooks.md), [ADR-009](adr/ADR-009-domain-knowledge-program.md), [ADR-010](adr/ADR-010-operational-knowledge-transition-unit.md), [[transition-reasoning]] + +## 1. Problem + +Wir produzieren drei Artefakte je Transition: **Transition Pattern**, **Playbook**, **Reference +Scenario**. Alle drei beschreiben dieselbe Geschichte aus verschiedenen Blickwinkeln. Wird derselbe +Übergang (z. B. ISO 9001 → MaschinenVO) in Pattern + Playbook + Szenario + Interview je separat +modelliert, **duplizieren wir die Transition** — genau das Problem, das wir bei Controls/Capabilities/ +Requirements mit der Identitätsmaschine gelöst haben. + +> Damals: *Controls nicht duplizieren.* Heute: **Transitionen nicht duplizieren.** + +## 2. Hypothese + +Es gibt unter den Artefakten ein gemeinsames Objekt — die **Journey** — als Wissenseinheit; alles +andere ist ein **Renderer** (*rendered, not modeled*). Diese Spezifikation **prüft die Hypothese an der +vorhandenen Transition ISO 27001 → CRA**, bevor irgendetwas gebaut wird. + +## 3. Die Wissenseinheit: Journey (Vorschlag) + +``` +Journey + identity: from (Ausgangszustand) → to (Zielzustand) # = transition_goal + source_variants: certified | introduced | expired | limited_scope # Start-Zustands-Varianten + likely_covered[]: { capability, relationship, confidence_source, verification, rationale, reviewable_claim, expected_evidence } + delta[]: { capability, why_missing, needed_information, expected_evidence, priority, dropped_if } + rejected_assumptions[] + # ── alles darunter ist ABGELEITET, nicht gespeichert ── + (derived) questions ← delta (RS-005) + (derived) measures ← delta × leverage (Optimization) + (derived) evidence_needs ← likely_covered.expected_evidence + delta.expected_evidence + (derived) verification ← evidence × Reality (Vision V2, künftig) +``` + +**Wahrheits-Hierarchie:** `Atomic Requirement → Capability → Journey (× Company-Kontext bei Instanz)`. +Alles andere wird gerendert. + +## 4. Renderer (rendered, not modeled) + +| Renderer | Was er zeigt | Quelle | +|---|---|---| +| **Transition Pattern** | die KURATIERTE generische Journey (likely_covered + delta + rationale) | = der **authored core** der Journey (gespeichert/kuratiert) | +| **Interview** | fehlende Informationen → Fragen | `Journey.delta` (RS-005) | +| **Roadmap** | Maßnahmen nach regulatorischem Hebel | `Journey.delta` × Leverage (Optimization) | +| **Reference Scenario** | erwartetes Ergebnis für ein Referenz-Unternehmen | **`Journey × Company/Product-Kontext`** + Expected Outcome | +| **Evidence-View** | welche Nachweise erfüllen die Anforderung | `Journey.evidence_needs` × Reality | +| **Rollen-Views** | Sales→Executive Summary · Auditor→Capability Delta · Developer→Evidence · GF→Roadmap | gefilterte Projektionen DER EINEN Journey | + +Vier Rollen-Views, aber **nur eine Journey**. + +## 5. Validierung an ISO 27001 → CRA (grounded, nicht hypothetisch) + +Geprüft gegen die echten Artefakte: `TP-ISO27001-CRA-v1.yaml`, die Playbooks `sbom_creation`/ +`coordinated_vulnerability_disclosure`, die Reference Scenarios `RTS-001/002/003`. + +| Artefakt | Felder | projiziert aus der Journey? | +|---|---|---| +| **Transition Pattern** | `transition_goal`, `source_state_variants`, `likely_covered[]` (relationship/rationale/reviewable_claim/expected_evidence), `delta_requirements[]` (why_asked/needed_information/expected_evidence/priority/dropped_if), `rejected_assumptions` | **JA — IST der authored core der Journey** (1:1). | +| **Interview** (RS-005) | `question_requests` aus `delta` | **JA — derived.** Kein eigenes Primärdatum. | +| **Roadmap** (Optimization) | Maßnahmen nach Leverage aus `delta` | **JA — derived.** | +| **Reference Scenario** | `reference_company{sector, certs, product_traits}` + `expected_outcome{cra, maschinenvo, data_act, convergence}` | **TEILWEISE — Journey × Company-Kontext.** Die Transition-Erkenntnis (likely_covered/delta) ist DIESELBE wie im Pattern; NEU ist nur der gebundene **Company/Product-Kontext** + die Expected-Outcome-Assertion (Regression). Kein dupliziertes Transitionswissen. | +| **Playbook** | `capability_id`, `why`, `tools`, `process_steps`, `expected_evidence`, `how_others_do_it` | **NEIN — pro CAPABILITY, nicht pro Journey.** `sbom_creation` wird von JEDER Journey wiederverwendet, deren Delta SBOM enthält. Bleibt capability-owned (ADR-004); die Journey **aggregiert** die Playbooks ihrer Delta-Capabilities, besitzt sie nicht. | + +## 6. Befund (ehrlich) + +**Die Vereinheitlichung HÄLT — mit zwei Präzisierungen, die eine voreilige Abstraktion VERMEIDEN:** + +1. **Pattern · Interview · Roadmap · Evidence · Rollen-Views = dieselbe Journey** (Pattern = kuratierter + Kern; der Rest derived). → *rendered, not modeled* gilt für die abgeleiteten Sichten. +2. **Reference Scenario = `Journey × Company-Kontext` + Expected Outcome** — kein zweites + Transitionsmodell, sondern die Journey instanziiert für ein Referenz-Unternehmen (Regression). +3. **Playbook = Capability-Renderer, NICHT Journey-Renderer** (andere Achse: Capability, nicht + Transition). Die Journey aggregiert, besitzt nicht. → das ist die früh erkannte Grenze, die + verhindert, dass wir Playbooks fälschlich in die Journey zwingen. + +**Konsequenz:** Die Transition wird genau EINMAL kuratiert (heute „Transition Pattern" = der Journey- +Kern). Interview/Roadmap/Reference-Outcome/Rollen-Views werden daraus gerendert. Playbooks bleiben +capability-owned und werden referenziert. Damit ist `ISO 9001 → MaschinenVO` (und jede weitere +Transition) ein EINZIGES kuratiertes Objekt, kein vierfach gepflegtes. + +## 7. Neues Prinzip (Vorschlag) + +Neben **computed, not stored** und **derived, not curated**: + +> **rendered, not modeled** — Transition Pattern, Playbook-Aggregat, Roadmap, Interview, Rollen-View +> sind keine eigenständigen Modelle, sondern Sichten auf Journey (+ Capability + Company-Kontext). + +## 8. Zwei Achsen (durable — die wichtigste Präzisierung) + +Der Playbook-Befund ist die wertvollste Erkenntnis: wir vereinheitlichen NICHT zwanghaft. Es gibt +**zwei unterschiedliche Achsen** mit getrennten Owner: + +``` +Atomic Requirement → Capability → Journey (Transition-Achse: „wie komme ich von A nach B?") +Capability → Playbook (Implementierungs-Achse: „wie baue ich Capability X?") +``` + +- **Journey** gehört der **Transition** (eine Journey je `from→to`). +- **Playbook** gehört der **Capability** (eine je Capability), von beliebig vielen Journeys referenziert. +Das ist eine stabile Trennung — Requirements gehören Requirements, Capabilities gehören Capabilities, +Journeys gehören Transitionen. + +## 9. Provisional Acceptance + Kanonisierungs-Hürde (Rule of Three) + +**Entscheidung (User 2026-06-28): „Accepted as Concept, Pending Canonicalization".** Journey ist die +bevorzugte Denkweise; das persistierte Artefakt bleibt vorerst `Transition Pattern`. Kein Rename, keine +Migration, keine Runtime-Änderung. + +### Kanonisierung an ZWEI Bedingungen (nicht nur „3 gebaut", User 2026-06-28) + +1. **≥ 3 BEWUSST UNTERSCHIEDLICHE Transitionen** erfolgreich modelliert (konsistent Pattern + + Interview + Roadmap + Reference Scenario). Je unterschiedlicher der Charakter, desto stärker die + Evidenz — NICHT drei ähnliche Security-Transitionen wählen: + + | Transition | Charakter | + |---|---| + | ✅ ISO 27001 → CRA | Security → Cyber-Regulierung | + | ⏳ ISO 9001 → MaschinenVO | Qualitätsmanagement → Produktsicherheit | + | ⏳ TISAX → CRA | Automotive Security → Cyber-Regulierung | + +2. **KEINE strukturelle Erweiterung des Journey-Modells in den letzten beiden Transitionen** (oder nur + klar begründete, ALLGEMEINGÜLTIGE Erweiterungen). Reifeindikator je Transition: **„Musste das Modell + ERWEITERT werden, oder wurden nur DATEN ergänzt?"** Bilanz: + + | Transition | Modelländerung erforderlich? | + |---|---| + | ISO 27001 → CRA | Ja (Erstentwurf) | + | ISO 9001 → MaschinenVO | _(offen — eintragen)_ | + | TISAX → CRA | _(offen — eintragen)_ | + +Erst wenn BEIDE Bedingungen erfüllt sind (3 diverse Transitionen + Modell in den letzten zwei stabil): +`Transition Pattern → Journey` umbenennen, **ADR-011** ratifizieren, Renderer offiziell daraus ableiten, +Prinzip *rendered, not modeled* aufnehmen. Das entspricht dem Muster bei Compiler / Layout-Familien / +Master Controls: **erst stabil unter UNTERSCHIEDLICHEN Belastungen, dann Standard.** + +## 10. Was das NICHT ist + +- **Kein Code, kein Runtime-Modul, keine neue Architektur** (Freeze unberührt). Konzeptionelles Modell. +- Non-runtime → kein Deploy (ADR-001). diff --git a/docs-src/architecture/transition-reasoning-spec-v1.md b/docs-src/architecture/transition-reasoning-spec-v1.md new file mode 100644 index 00000000..3ab94a28 --- /dev/null +++ b/docs-src/architecture/transition-reasoning-spec-v1.md @@ -0,0 +1,161 @@ +# Transition Reasoning — Spezifikation v1.3 (zweiter Reasoning-Modus) + +- **Status:** Proposal / Spec — 2026-06-27. +- **v1.1:** Interviewfragen werden **aus den Controls GENERIERT**, nicht als Bibliothek geschrieben. +- **v1.2:** **Wissensproduktion durch KI** — LLMs erzeugen den ersten Expertenentwurf (v. a. die + Priorisierung je Transition); BreakPilot **reviewt, versioniert und besitzt** die kanonische + Bibliothek. Plus die juristische Grenze (Expertenentwurf, kein Normbeweis). +- **v1.3 (Architektur präzisiert):** Die **Transition Planning Engine** besitzt die + **Informationslücken** (`TransitionQuestionRequest`), NICHT die Fragen. Kette: + `Planning Engine → TransitionQuestionRequest → Question Renderer (RS-005.1) → Interview`. + **RS-005 v0 (Planning Engine) GEBAUT** (`compliance/transition_reasoning/`, kein Endpoint, + konsumiert Company 2A + injizierte `TargetRequirement`). **RS-005.1 (Renderer/Templates) bewusst + VERSCHOBEN** — erst Wissensproduktion (kuratierte Transition Patterns in eigener Wissenssession), + dann der Renderer (ohne Inhalte wertlos; Inhalte ohne Renderer sofort wertvoll). +- **Scope:** erweitert die Reasoning-Session um einen zweiten Modus; ersetzt nichts. +- **Freeze-konform:** additiv, **kein** neuer Graph, **keine** Basisklasse, **keine** Meta-Model-Klasse. Nur ein neues Paket + Daten. + +## Paradigmenwechsel + +BreakPilot beantwortet **Migrationsfragen**, nicht Regelwerk-Fragen. Nicht `Regelwerk → Gap`, +sondern **`Ausgangszustand → Zielzustand → Delta`**. Ziel beliebig: `ISMS→TISAX`, `DSGVO→CRA`, +`ISO9001→IATF16949`, `MaschRL→MaschVO`, `CRA2025→CRA2028`. **Immer dieselbe Engine.** + +## Kerneinsicht v1.1: Fragen sind ein DERIVAT, kein Bestand + +Ihr besitzt bereits den teuren Teil: **~14.000 Master Controls** (aus ~400.000 atomaren Controls), +Legal Obligations, Capability Registry, Regulatory Map. Jedes Gesetz liefert deterministisch +`Gesetz → Obligation → Capability → Control → Evidence`. **Also dreht man die Richtung um:** + +```text +Regelwerk → Obligation → Capability → Control → "welche Information fehlt, um diesen Control zu bewerten?" +``` + +Die Frage entsteht **aus dem Control**. Control `CTRL-712: „A documented vulnerability handling +process exists." (intent: verify_existence)` → automatisch „Gibt es einen dokumentierten +Vulnerability-Handling-Prozess?". Ingestiert ihr morgen die Abwasserverordnung, entstehen mit den +neuen Controls **automatisch** neue Interviewfragen. Passt zu *derived, not curated* + *computed, not stored*. + +## Die drei Bausteine (statt einer „ISO-27001-Fragenbibliothek") + +### 1. Control → Question Intent *(Corpus-/Control-Domäne — Compliance Execution / canonical_controls)* +Jeder kanonische Control trägt einen `question_intent`: +```yaml +CTRL-712: {statement: "A documented vulnerability handling process exists.", question_intent: verify_existence} +CTRL-145: {statement: "Security updates are provided for a defined period.", question_intent: determine_duration} +``` +Annotation am Control, **kein** neuer Reasoning-Bestand. + +### 2. Master Question Templates *(Reasoning — klein, ~30–40, versioniert)* +```yaml +verify_existence: "Gibt es einen dokumentierten {{subject}}?" +determine_duration: "Wie lange werden {{subject}} bereitgestellt?" +identify_owner: "Wer verantwortet {{subject}}?" +request_evidence: "Welchen Nachweis können Sie für {{subject}} bereitstellen?" +# ~30–40: existiert? / verantwortlich? / seit wann? / wie häufig? / wie dokumentiert? / +# welcher Nachweis? / wie lange? / wie geprüft? / wie gemessen? / wie freigegeben? +``` +**Generierung:** `Control(statement + intent) × Template → Frage`. Die ~30–40 Templates **emergieren** +durch Clustern der Control-Statements nach Intent (dieselbe Dedup-Logik wie Master Controls). Das ist +die eigentliche „MDQ Registry": **kanonische Interview-Templates**, nicht handgeschriebene Fragen. + +### 3. Transition-Priorisierung *(Reasoning — hier steckt das Expertenwissen)* +Zertifizierungen / Managementsysteme entscheiden, **welche** generierten Fragen noch gestellt werden: +```text +CRA → 217 Controls → 217 mögliche Fragen + ISO27001 → 165 „wahrscheinlich beantwortet" → 52 übrig + + TISAX → 31 · + PSIRT → 24 · + SBOM → 19 ⟶ nur diese 19 werden gestellt. +``` +Mechanik (**Wiederverwendung**): `Control → Capability` (Corpus) ∘ `Capability → wahrscheinlich-vorhanden` +(Cert→Capability-Mapping = Execution, Company 2A) → Control überspringen, wenn Capability `confirmed`/`probably`. + +## Wissensproduktion (v1.2): KI erzeugt den Entwurf — BreakPilot besitzt die Bibliothek + +Die Priorisierung (Baustein 3: „welche Bereiche deckt ISO 27001 für CRA typischerweise ab, wo sind die +Lücken?") ist **Expertenwissen**, keine Rechtsauslegung — genau das, was Berater heute leisten („Sie sind +ISO-27001-zertifiziert? Dann nehme ich ISMS/Incident/Lieferantenmgmt als gegeben an; reden wir über SBOM, +PSIRT, Security Updates."). **Das kann ein LLM als ersten Entwurf liefern.** Prozess umgedreht: nicht „LLM +beantwortet Compliance" — sondern „**LLM erstellt den Entwurf der Interview-/Priorisierungs-Bibliothek**". + +**Offline-Workflow (NICHT zur Laufzeit):** +```text +LLM-A (z.B. "Du bist ISO27001 Lead Auditor") → Entwurf: skip-Liste + CRA-Delta-Fragen je Transition +LLM-B → kritisiert / findet Lücken LLM-C → ergänzt fehlende → Merge +→ BreakPilot-Review → versionierte MDQ/Transition Registry (Git) → Kundenfeedback → v2 → v3 +``` +Multi-Modell (Draft / Kritik / Ergänzung), dann menschlicher Review. **In Git landet die Bibliothek, +nicht die KI.** Das ist die *deterministisch-first*-Disziplin (LLM offline-propose, nie online-mutate). + +**Eigentumswechsel:** nach ~50 Kunden ist die Bibliothek **kein Modellwissen mehr**, sondern euer +**kuratiertes, versioniertes Unternehmenswissen**. Die Engine arbeitet gegen `MDQ-00127 · Version 4 · +reviewed 2026-09-18 · Freigabe BreakPilot` — **modell-unabhängig** (egal was GPT-7/Claude-6 morgen sagen). +Dieselbe Identitätsmaschine + Provenance wie Controls/Obligations/Capabilities. + +**Initiative (Vorschlag): „1000 Master Delta Questions in 30 Tagen"** — strukturiert: (1) Zielregelwerk +wählen · (2) Ausgangszustände wählen (ISO27001/TISAX/ISO9001/ISO14001/IEC62443) · (3) LLM erstellt +Delta-Katalog · (4) zweites Modell prüft Lücken/Redundanz · (5) Review + Versionierung · (6) Kundenfeedback. +KI erzeugt den ersten Entwurf; **BreakPilot besitzt + pflegt die kanonische versionierte Bibliothek.** + +## ⚠️ Grenze: Expertenentwurf, KEIN Normbeweis (durable) + +Die Bibliothek ist ein **plausibler Expertenentwurf** (Berater-Heuristik), **NICHT** ein juristischer/ +normativer Beweis, dass die Fragen exakt die Norm repräsentieren. Die Priorisierung „Zertifizierung deckt +X wahrscheinlich ab" ist **Welt 1** (`ClaimCoverage` — potenziell, mit Confidence + Verifikationsbedarf), +**nie** „erfüllt"/Norm-Abdeckungs-Beweis (Welt 2). Muss so gelabelt sein — sonst genau das +Compliance-Theater, das `anti-fake-evidence` verhindert. Eine `inferred`-Annahme aus „ISO27001 vorhanden" +wird erst durch echten Nachweis `confirmed`. + +## Kernobjekte (`compliance/transition_reasoning/`) + +- **`TransitionContext`** — `start_state` {company_context, product_profile, known_regulations/certifications, capabilities (2A)} + `target_state` {target_regulation | target_certification | target_framework}. +- **`TransitionGoal`** — Ziel (nicht regelwerk-beschränkt); auflösbar zu Obligations → Controls → Required Capabilities. +- **`MasterQuestionTemplate`** — `{intent, template_text, answer_type, expected_evidence, version, status, lifecycle}`. +- **`TargetRequirement`** (INJIZIERT, Execution-owned) — `{capability_id (MCAP), question_intent, expected_evidence, source_control_id, supports_obligations, unsupported}`. v0: injiziert; später aus `Obligation → Control → Required Capability` + `Control → question_intent`. +- **`TransitionQuestionRequest`** (das **OWNED Output** der Planning Engine — eine Informationslücke, **KEINE** Frage) — `{capability_id, control_id, reason, question_intent, expected_evidence, priority, information_gain}`. **Kein gerenderter Fragetext.** +- **`TransitionAssessment`** (Output, **KEINE** Prozentzahlen) — je Required Capability `CoverageStatus ∈ {already_covered, probably_covered, needs_confirmation, missing, not_applicable, unsupported}` + die priorisierten `TransitionQuestionRequest`s. +- **`GeneratedQuestion`** *(RS-005.1 Question Renderer — VERSCHOBEN, nicht in v0)* — `Request × (Control/Template) → {rendered_text, …}`. Das Rendern ist eine **austauschbare Policy-Schicht**, nicht Teil der Engine. + +## Algorithmus — Delta-Interview als Optimierung + +1. `TransitionGoal` → Obligations → Controls → Required Capabilities (MCAP). +2. `have` = Company Capability Profile (confirmed ∪ inferred ∪ declared) — 2A. +3. **Priorisierung/Skip** (Baustein 3): Controls überspringen, deren Capability wahrscheinlich vorhanden ist. +4. Übrige Controls: Frage via `Control × Template` **generieren** (Baustein 1+2), durch Product Map filtern. +5. **Nicht sequenziell** — iterativ die Frage mit dem **höchsten erwarteten Informationsgewinn** (Entropy Reduction) wählen; Antwort → `confidence_impact` → Profil aktualisiert → neu priorisieren. Abbruch, wenn nichts mehr nennenswert reduziert (**Ziel: 10–20**). +6. Ergebnis: aktualisiertes Capability Profile → `TransitionAssessment`. + +**Information Gain** deterministisch + erklärbar (v1 `HIGH/MEDIUM/LOW`; v2 aus `observed`-Statistik +asked/changed/useless — Statistik gespeichert, Score abgeleitet; kein opaker ML-Score). **Confidence +Impact**: eine Antwort liefert Relation+Evidence → bestehende `evaluate_relation`-Policy (2C) berechnet den +Status neu (`inferred→confirmed`); die Frage speichert keine Confidence. + +## Ownership (Freeze- und Domänen-konform) + +| Domäne | besitzt | +|---|---| +| **Legal Knowledge** | Ziel-Regelwerke + Obligations | +| **Compliance Execution / canonical_controls** | (Master) Controls + `Control→question_intent`, `Control/Obligation→Required Capability`, MCAP, Cert→Capability-Mapping | +| **Reasoning (dieser Modus)** | Transition Engine, **Master Question Templates**, Fragen-Generierung, **Priorisierung/Skip**, Information Gain, Delta Interview, Assessment, kuratierte/versionierte **MDQ/Transition Registry** | + +Reasoning **konsumiert** Controls + Required Capabilities + Cert→Capability-Mapping (injiziert; keine +Corpus-/Mapping-Daten im Produktcode). Keine neuen Mappings/Regelwerke/Controls, keine Meta-Model-Klasse. + +## Akzeptanzkriterien (Demos → neue Reference-Suite-Szenarien) + +- **ISMS → TISAX:** < 20 Fragen. · **DSGVO → CRA:** erkennt Datenschutz, fragt SBOM / Vuln Handling / Security Updates / Product Security. · **ISO9001 → IATF16949:** nur Delta-Fragen. · **CRA2025 → CRA2028:** RCI liefert Delta, Engine formuliert Interview. Jede Demo = Coverage-Zeile in der [[reference-scenario-suite]]. + +## Architektur-Invariante (→ [ADR-002](adr/ADR-002-transition-is-data-not-architecture.md)) + +1. **Keine vollständigen Regelwerke als Interviews — nur der minimale Informationsgewinn vom Ausgangs- in den Zielzustand.** +2. **Jede neue Transition entsteht AUSSCHLIESSLICH durch Daten** (Controls + question_intent, Cert→Capability-Priorisierung, ggf. neue Templates); Engine + Metamodell werden NICHT erweitert. → Interview-Engine wächst automatisch mit jedem Corpus; Handarbeit nur bei der Priorisierung. + +## Naming & Sequenzierung + +„Onboarding-Fragen" → **Transition Interview**. Abhängigkeit: `Control→question_intent` + `Control/Obligation→Capability` ++ Cert→Capability-Mapping (Execution/canonical_controls). Baubar als **v0** mit injizierten Controls/Mappings + +den ~30–40 Templates; v2 (computed Information Gain + Lern-Statistik) folgt. = Epic **RS-005** (verwandt mit RS-003). +**Spec jetzt, Bau pro Go; die MDQ-Bibliothek ist Wissensproduktion (Workflow), kein Code-Build.** + +## Anhang + +- 100 Beispiel-Fragen (v1) = Validierungsset + Quelle für Template-/Intent-Extraktion (nicht die Bibliothek): [`transition-reasoning/master-delta-questions-v1.md`](transition-reasoning/master-delta-questions-v1.md). diff --git a/docs-src/architecture/transition-reasoning/master-delta-questions-v1.md b/docs-src/architecture/transition-reasoning/master-delta-questions-v1.md new file mode 100644 index 00000000..bc2f2ac3 --- /dev/null +++ b/docs-src/architecture/transition-reasoning/master-delta-questions-v1.md @@ -0,0 +1,148 @@ +# Master Delta Questions — v1 (Seed-Library) + +- **Status:** v1 Seed — 2026-06-27. Gehört zu [`../transition-reasoning-spec-v1.md`](../transition-reasoning-spec-v1.md). +- **Sortierung nach Operational Capability, nicht nach Regelwerk** — dadurch werden Fragen über + Transitions hinweg wiederverwendbar (dieselbe Frage ist für CRA, IEC 62443, NIS2 und teils TISAX relevant). + +## Herkunft der Fragen (warum es wenige sind) + +- **Regulatorische Pflichten** (CRA, MaschVO, NIS2, Data Act, Umweltrecht …) sagen, **welche + Capability benötigt** wird. +- **Bestehende Zertifizierungen** (ISO 27001, TISAX, ISO 9001, ISO 14001, IATF 16949 …) sagen, + **welche Capability wahrscheinlich bereits existiert**. +- Die Bibliothek schließt **nur die Unsicherheit dazwischen** → dieselbe Frage in vielen Übergängen + wiederverwendbar. Erwartung: am Ende **~300–500 Master Delta Questions**, nicht 5.000. + +## Rolle dieser Liste (nach Spec-Revision v1.1/v1.2) + +Diese 100 sind **NICHT die Bibliothek**. Laut Spec v1.1 werden Fragen aus den **Controls generiert** +(`Control × Master Question Template`), nicht von Hand geschrieben; das Expertenwissen steckt in der +**Priorisierung** (welche Frage eine Zertifizierung überspringbar macht), die laut v1.2 als LLM-Entwurf +entsteht und dann von BreakPilot **reviewt + versioniert + besessen** wird. Diese 100 dienen daher als: +- **Validierungsset** — entsteht aus dem Generate-from-Controls-Pfad eine vergleichbare Frage? +- **Quelle für die ~30–40 Master Question Templates** (Intents per Clustern extrahieren). +- **Seed/Beispiel** für den „1000 MDQ in 30 Tagen"-Workflow. + +Siehe [`../transition-reasoning-spec-v1.md`](../transition-reasoning-spec-v1.md) (v1.2). + +--- + +## Bereich A — Unternehmenskontext (1–10) +1. Welche Managementsysteme sind aktuell im Unternehmen eingeführt? +2. Welche Zertifizierungen sind derzeit gültig? +3. Welche Zertifizierungen befinden sich aktuell in Vorbereitung? +4. Welche regulatorischen Anforderungen möchten Sie als Nächstes erfüllen? +5. Welche Länder und Märkte beliefern Sie? +6. Welche Rolle nehmen Sie ein (Hersteller, Integrator, Importeur, Betreiber, Service)? +7. Entwickeln Sie eigene Produkte oder integrieren Sie Komponenten Dritter? +8. Entwickeln Sie eigene Software oder Firmware? +9. Welche Nachweise können Sie heute bereits unmittelbar bereitstellen? +10. Welche regulatorischen Themen bereiten Ihnen derzeit die größten Schwierigkeiten? + +## Bereich B — Produktentwicklung (11–20) +11. Gibt es einen dokumentierten Entwicklungsprozess? +12. Werden Anforderungen versioniert? +13. Werden Änderungen nachvollziehbar dokumentiert? +14. Werden Architekturentscheidungen dokumentiert? +15. Existieren Design Reviews? +16. Werden Sicherheitsanforderungen bereits in der Entwicklung berücksichtigt? +17. Gibt es definierte Freigabekriterien? +18. Existiert ein Releaseprozess? +19. Werden Softwareversionen eindeutig identifiziert? +20. Existiert eine Produktstückliste (BOM)? + +## Bereich C — Software & Firmware (21–30) +21. Enthält Ihr Produkt Software? +22. Enthält Ihr Produkt Firmware? +23. Werden Firmwareupdates ausgeliefert? +24. Erfolgen Updates lokal oder remote? +25. Können Updates automatisiert verteilt werden? +26. Werden Updatepakete signiert? +27. Wird die Integrität von Updates geprüft? +28. Existiert eine Rollback-Strategie? +29. Werden Softwarekomponenten versioniert? +30. Werden Drittanbieterbibliotheken dokumentiert? + +## Bereich D — SBOM & Komponenten (31–40) +31. Wird für jede Version automatisch eine SBOM erzeugt? +32. Welches SBOM-Format verwenden Sie? +33. Wird die SBOM versioniert? +34. Enthält die SBOM alle Softwarekomponenten? +35. Enthält sie Open-Source-Komponenten? +36. Enthält sie proprietäre Komponenten? +37. Wird die SBOM Kunden bereitgestellt? +38. Wird sie intern gepflegt? +39. Wird sie automatisiert erzeugt? +40. Wie wird ihre Aktualität sichergestellt? + +## Bereich E — Vulnerability Management (41–50) +41. Existiert ein dokumentierter Vulnerability-Management-Prozess? +42. Wer bewertet gemeldete Schwachstellen? +43. Wie werden CVEs verfolgt? +44. Gibt es definierte Reaktionszeiten? +45. Werden Schwachstellen priorisiert? +46. Existiert ein PSIRT? +47. Gibt es einen Meldeprozess für Kunden? +48. Werden Security Advisories veröffentlicht? +49. Werden behobene Schwachstellen dokumentiert? +50. Wird der Prozess regelmäßig überprüft? + +## Bereich F — Security Updates (51–60) +51. Gibt es eine dokumentierte Update-Richtlinie? +52. Wie lange liefern Sie Sicherheitsupdates? +53. Wie informieren Sie Kunden über Updates? +54. Wie wird die Authentizität von Updates sichergestellt? +55. Werden Notfallupdates unterstützt? +56. Können Updates zurückgezogen werden? +57. Können Kunden Updates ablehnen? +58. Werden Updatefehler protokolliert? +59. Gibt es einen definierten Update-Lifecycle? +60. Wer verantwortet den Updateprozess? + +## Bereich G — Lieferanten (61–70) +61. Werden Lieferanten sicherheitsbezogen bewertet? +62. Gibt es Sicherheitsanforderungen an Lieferanten? +63. Werden Softwarelieferanten regelmäßig überprüft? +64. Werden Lieferantenänderungen dokumentiert? +65. Werden Sicherheitsvorfälle von Lieferanten verfolgt? +66. Werden Lieferantenverträge regelmäßig überprüft? +67. Existieren Mindestanforderungen für Softwarelieferanten? +68. Werden Lieferanten auditiert? +69. Gibt es einen Eskalationsprozess? +70. Wie werden kritische Lieferanten identifiziert? + +## Bereich H — Betrieb & Support (71–80) +71. Existiert ein Incident-Response-Prozess? +72. Gibt es einen Security-Support? +73. Werden Sicherheitsvorfälle dokumentiert? +74. Gibt es definierte Eskalationsstufen? +75. Werden Kunden über Vorfälle informiert? +76. Existiert ein Backupkonzept? +77. Gibt es Wiederherstellungstests? +78. Werden Supportanfragen klassifiziert? +79. Gibt es definierte Servicezeiten? +80. Werden Lessons Learned dokumentiert? + +## Bereich I — Nachweise (81–90) +81. Welche Richtlinien können Sie unmittelbar bereitstellen? +82. Welche Prozesse sind dokumentiert? +83. Welche Arbeitsanweisungen existieren? +84. Welche Auditberichte liegen vor? +85. Welche Zertifikate liegen aktuell vor? +86. Welche technischen Nachweise können Sie liefern? +87. Welche Protokolle werden archiviert? +88. Welche Nachweise werden versioniert? +89. Welche Nachweise sind öffentlich verfügbar? +90. Welche Nachweise fehlen derzeit? + +## Bereich J — Neue regulatorische Anforderungen (91–100) +91. Erzeugen Sie Nutzungsdaten Ihrer Produkte? +92. Unterstützen Ihre Produkte Fernwartung? +93. Enthalten Ihre Produkte Funkmodule? +94. Werden sicherheitsrelevante Ereignisse protokolliert? +95. Gibt es Umweltaspekte wie Chemikalien oder Abwasser? +96. Werden Umweltmessdaten dokumentiert? +97. Gibt es produktspezifische Entsorgungsanforderungen? +98. Gibt es dokumentierte Cyber-Risikoanalysen? +99. Welche neuen regulatorischen Anforderungen sehen Sie selbst als größte Herausforderung? +100. Welche regulatorischen Nachweise möchten Sie innerhalb der nächsten zwölf Monate erstmals oder zusätzlich erfüllen? diff --git a/obligations/PROPOSED_machinery_capability_linking_iace.json b/obligations/PROPOSED_machinery_capability_linking_iace.json new file mode 100644 index 00000000..aff729ba --- /dev/null +++ b/obligations/PROPOSED_machinery_capability_linking_iace.json @@ -0,0 +1,326 @@ +{ + "schema_proposal": "machinery_obligation_capability_linking_v0", + "status": "PROPOSED", + "proposed_by": "iace-session", + "for_ratification_by": ["legal-knowledge-graph", "execution"], + "reference_scenario": "RS-004", + "regulation_code": "MaschVO_2023_1230", + "regulation_aliases": ["MaschinenVO", "Machinery Regulation (EU) 2023/1230"], + "authority_note": "IACE holds SAFETY-classification authority and offers these links as machinery-safety domain input. Obligation DEFINITIONS remain the Legal-KG's authority; capability/control MINTING and the canonical mapping FORMAT remain Execution's authority. Nothing here is asserted into either registry. cap.* ids on physical/process links are CANDIDATES (not minted) — ratify, rename, or remap before merging into the canonical mapping. See semantic-authority principle: propose, do not assert across authorities.", + "scope": { + "in_scope": "MaschVO obligation -> capability/control linking (RS-004 part A), from the machinery-safety side.", + "out_of_scope": [ + "EMV (EMC Directive 2014/30/EU) obligation authoring (RS-004 part B): EMV obligations do not yet exist in the registry. Legal-KG to author via its clustering+synthesis methodology. IACE can supply EMC domain input on request, but will not hand-author obligations (bypasses the owning authority's method).", + "Regulation-ID normalization / scope-engine wiring so the map resolves regulation -> obligations (RS-004 part C): Reasoning/Execution consumer code. NOTE: regulation_code 'MaschVO_2023_1230' must alias to the scope-engine id 'MaschinenVO' for resolution to work (board TODO 'Regelwerk-ID-Normalisierung').", + "Minting MCAP-/control-ids: Execution authority." + ] + }, + "confidence_legend": { + "high": "Link target already exists in the registry (cra_core obligation or minted capability). Immediately usable.", + "medium": "Link target likely exists but the exact id needs an owner check.", + "proposed": "Target capability is a CANDIDATE to be minted by Execution; the link is safety-expert input, not a wired reference.", + "non_capability": "Obligation is regulatory/applicability in nature and does NOT map to a capability — flagged so Execution does not force a link." + }, + "links": [ + { + "obligation_id": "access_control_safety_functions", + "subdomain": "cybersecurity", + "link_kind": "cyber_safety_bridge", + "confidence": "high", + "targets_existing": { + "cra_core_obligations": ["attack_surface_minimization"], + "capabilities": ["cap.multi_factor_authentication", "cap.session_management"] + }, + "rationale": "MaschVO Anhang III 1.1.9: safety functions must be protected against unauthorized access/modification. Satisfied by the same access-control + attack-surface controls CRA already requires. Convergence link, not a new control.", + "convergence": "CRA <-> MaschinenVO: one control set satisfies both" + }, + { + "obligation_id": "protection_against_corruption", + "subdomain": "cybersecurity", + "link_kind": "cyber_safety_bridge", + "confidence": "high", + "targets_existing": { + "cra_core_obligations": ["software_integrity_protection"], + "capabilities": ["cap.code_signing"] + }, + "rationale": "MaschVO 1.1.9/1.2.1: protect control software and safety-relevant data against accidental or intentional corruption. Satisfied by CRA software-integrity + code/update signing.", + "convergence": "CRA <-> MaschinenVO: one control set satisfies both" + }, + { + "obligation_id": "security_functions_default_free", + "subdomain": "cybersecurity", + "link_kind": "cyber_safety_bridge", + "confidence": "medium", + "targets_existing": { + "cra_core_obligations": ["secure_by_default"], + "capabilities": [] + }, + "rationale": "Security functions provided secure-by-default and without extra cost. Maps to CRA secure-by-default posture.", + "needs_owner_check": "Confirm a CRA 'secure_by_default' obligation id exists in cra_core; if not, propose one or link to the closest secure-configuration obligation." + }, + { + "obligation_id": "ml_safety_components", + "subdomain": "ml_safety", + "link_kind": "cross_regulation_bridge", + "confidence": "proposed", + "proposed_capability": "cap.ml_safety_assurance", + "bridges": ["AI-Act (high-risk safety components)", "MaschVO Anhang III adaptive behaviour"], + "iace_grounding": "Adaptive/self-learning safety components: bounded behaviour, validation of learned states, fallback to safe state. IACE state-graph + failure-mode (FMEA) families apply.", + "rationale": "MaschVO treats ML-driven safety components as high-risk; same assurance obligations recur under the AI-Act. Strong convergence candidate." + }, + { + "obligation_id": "long_term_availability_updates", + "subdomain": "maintenance", + "link_kind": "cross_regulation_bridge", + "confidence": "proposed", + "proposed_capability": "cap.update_availability", + "bridges": ["CRA vulnerability-handling / security updates"], + "rationale": "Long-term availability of (security) updates overlaps CRA's vulnerability-handling obligations — link once the CRA update obligation id is confirmed." + }, + { + "obligation_id": "guards_protective_devices", + "subdomain": "protective_devices", + "link_kind": "physical_safety", + "confidence": "proposed", + "proposed_capability": "cap.guards_protective_devices", + "registry_candidate": true, + "iace_grounding": "ISO 14120 (fixed/movable guards), ISO 14119 (interlocking with/without guard locking). IACE hazard categories: mechanical, crushing, shearing, drawing-in.", + "rationale": "Already listed in cra_machinery capability_candidates_physical. Safety-expert grounding attached." + }, + { + "obligation_id": "emergency_stop_interlocking", + "subdomain": "safety_functions", + "link_kind": "physical_safety", + "confidence": "proposed", + "proposed_capability": "cap.emergency_stop_interlocking", + "registry_candidate": true, + "iace_grounding": "ISO 13850 (emergency stop), ISO 14118 (prevention of unexpected start-up), ISO 14119 (interlocking).", + "rationale": "Already listed in cra_machinery capability_candidates_physical. Safety-expert grounding attached." + }, + { + "obligation_id": "safety_functions_design", + "subdomain": "safety_functions", + "link_kind": "physical_safety", + "confidence": "proposed", + "proposed_capability": "cap.safety_functions_design", + "registry_candidate": true, + "iace_grounding": "ISO 13849-1 (PL, categories) / IEC 62061 (SIL) for safety-related parts of control systems (SRP/CS); validation per ISO 13849-2.", + "rationale": "Already listed in cra_machinery capability_candidates_physical. Safety-expert grounding attached." + }, + { + "obligation_id": "safety_components_conformity", + "subdomain": "safety_components", + "link_kind": "physical_safety", + "confidence": "proposed", + "proposed_capability": "cap.safety_component_conformity", + "iace_grounding": "Listed safety components (MaschVO Anhang I) carry their own conformity duty; design validation per ISO 13849-2.", + "rationale": "Distinct from safety_functions_design: this is conformity of the COMPONENT placed on the market, not the integrated function." + }, + { + "obligation_id": "residual_risk_management", + "subdomain": "residual_risk", + "link_kind": "physical_safety", + "confidence": "proposed", + "proposed_capability": "cap.residual_risk_reduction", + "iace_grounding": "ISO 12100 three-step method (inherently safe design -> safeguarding -> information for use); residual-risk warnings + instructions.", + "rationale": "Directly mirrors IACE's measure-hierarchy output." + }, + { + "obligation_id": "blocking_release_procedure", + "subdomain": "protective_devices", + "link_kind": "physical_safety", + "confidence": "proposed", + "proposed_capability": "cap.energy_isolation_loto", + "iace_grounding": "ISO 14118 (unexpected start-up), lockout/tagout, safe isolation of energy sources for maintenance.", + "rationale": "Maintenance-state hazard control; IACE lifecycle-state = maintenance." + }, + { + "obligation_id": "vibration_noise_emission", + "subdomain": "emissions", + "link_kind": "physical_safety", + "confidence": "proposed", + "proposed_capability": "cap.emission_reduction", + "iace_grounding": "EHSR on vibration + noise; emission reduction at source, declared emission values.", + "rationale": "Health-hazard category in IACE (vibration, noise)." + }, + { + "obligation_id": "risk_assessment_machinery_lifecycle", + "subdomain": "risk_assessment", + "link_kind": "process", + "confidence": "proposed", + "proposed_capability": "cap.machinery_risk_assessment", + "iace_grounding": "ISO 12100 risk assessment across the full lifecycle. THIS IS IACE'S CORE OUTPUT — strongest provider-fact alignment of the set.", + "rationale": "IACE already produces lifecycle hazard logs; this obligation is the regulatory counterpart." + }, + { + "obligation_id": "risk_assessment_documentation", + "subdomain": "risk_assessment", + "link_kind": "process", + "confidence": "proposed", + "proposed_capability": "cap.risk_assessment_record", + "iace_grounding": "Documented risk-assessment record feeding the technical file.", + "rationale": "IACE hazard-log export is the evidence artifact." + }, + { + "obligation_id": "risk_assessment_methodology_competence", + "subdomain": "risk_assessment", + "link_kind": "process", + "confidence": "proposed", + "proposed_capability": "cap.risk_assessment_competence", + "tier": "BEST_PRACTICE", + "rationale": "Competence/methodology assurance for the assessor — organizational, not a machine control." + }, + { + "obligation_id": "operating_instructions", + "subdomain": "operating_instructions", + "link_kind": "process", + "confidence": "proposed", + "proposed_capability": "cap.safety_information_instructions", + "iace_grounding": "ISO 12100 6.4 information for use; IEC/IEEE 82079-1 instructions.", + "rationale": "Carries IACE residual-risk warnings into the instructions." + }, + { + "obligation_id": "conformity_assessment", + "subdomain": "conformity", + "link_kind": "process", + "confidence": "proposed", + "proposed_capability": "cap.conformity_assessment_procedure", + "iace_grounding": "MaschVO Anhang XI procedures (internal control vs notified-body routes).", + "rationale": "Procedure selection depends on Anhang I high-risk classification." + }, + { + "obligation_id": "technical_documentation", + "subdomain": "documentation", + "link_kind": "process", + "confidence": "proposed", + "proposed_capability": "cap.technical_file", + "iace_grounding": "MaschVO Anhang IV technical file; risk assessment is a mandatory part.", + "rationale": "IACE hazard log is a required input to the technical file." + }, + { + "obligation_id": "eu_declaration_ce_marking", + "subdomain": "conformity", + "link_kind": "process", + "confidence": "proposed", + "proposed_capability": "cap.ce_marking_declaration", + "iace_grounding": "MaschVO Anhang V EU declaration of conformity + CE marking affixing.", + "rationale": "Final conformity attestation step." + }, + { + "obligation_id": "manufacturer_economic_operator_obligations", + "subdomain": "economic_operators", + "link_kind": "process", + "confidence": "proposed", + "proposed_capability": "cap.economic_operator_duties", + "rationale": "Manufacturer/importer/distributor duty chain — organizational." + }, + { + "obligation_id": "essential_safety_requirements_compliance", + "subdomain": "ehsr", + "link_kind": "process", + "confidence": "proposed", + "proposed_capability": "cap.ehsr_compliance", + "iace_grounding": "MaschVO Anhang III essential health and safety requirements — the umbrella that the physical_safety capabilities collectively satisfy.", + "rationale": "Composite: satisfied via the physical_safety capabilities above; model as an aggregate rather than a single control." + }, + { + "obligation_id": "harmonised_standards_selection", + "subdomain": "standards", + "link_kind": "process", + "confidence": "proposed", + "proposed_capability": "cap.harmonised_standards", + "tier": "BEST_PRACTICE", + "iace_grounding": "Use of harmonised standards grants presumption of conformity; IACE's ISO references (12100/13849/14120/13850) are the candidate set.", + "rationale": "Links the standards IACE already cites to the presumption-of-conformity mechanism." + }, + { + "obligation_id": "notified_body_requirements", + "subdomain": "notified_body", + "link_kind": "process", + "confidence": "proposed", + "proposed_capability": "cap.notified_body_involvement", + "iace_grounding": "MaschVO Anhang I Part A high-risk machinery requires notified-body involvement.", + "rationale": "Triggered by Anhang I classification of the machine." + }, + { + "obligation_id": "modification_substantial_change", + "subdomain": "modification", + "link_kind": "process", + "confidence": "proposed", + "proposed_capability": "cap.substantial_modification_assessment", + "iace_grounding": "Substantial modification can create a 'new' machine requiring fresh conformity; re-run risk assessment.", + "rationale": "IACE re-assessment is the trigger artifact." + }, + { + "obligation_id": "autonomous_mobile_machinery", + "subdomain": "mobile_machinery", + "link_kind": "physical_safety", + "confidence": "proposed", + "proposed_capability": "cap.amr_safety", + "iace_grounding": "Mobile/autonomous machinery EHSR: travel functions, supervision, monitoring, safe stop in autonomous mode.", + "rationale": "Distinct hazard family (mobility) in IACE." + }, + { + "obligation_id": "verification_inspection_maintenance", + "subdomain": "verification", + "link_kind": "process", + "confidence": "proposed", + "proposed_capability": "cap.in_service_verification", + "tier": "BEST_PRACTICE", + "rationale": "In-service inspection/maintenance regime — lifecycle-state = in_service/maintenance." + }, + { + "obligation_id": "quality_management_system", + "subdomain": "quality_management", + "link_kind": "process", + "confidence": "proposed", + "proposed_capability": "cap.quality_management_system", + "tier": "BEST_PRACTICE", + "iace_grounding": "MaschVO Anhang IX full quality-assurance route.", + "rationale": "Organizational QA enabling the conformity route." + }, + { + "obligation_id": "market_surveillance_safeguard", + "subdomain": "market_surveillance", + "link_kind": "non_capability", + "confidence": "non_capability", + "rationale": "Cooperation with market-surveillance authorities + safeguard procedure: a regulatory-interaction duty, not a machine/process capability. Flagged so Execution does not force a capability link.", + "owner_decision": "Legal-KG to decide whether to model as an obligation-only node." + }, + { + "obligation_id": "sanctions", + "subdomain": "sanctions", + "link_kind": "non_capability", + "confidence": "non_capability", + "rationale": "Penalty regime — a legal consequence, not a capability. No control link.", + "owner_decision": "Legal-KG: obligation-only node." + }, + { + "obligation_id": "scope_transition_application", + "subdomain": "scope", + "link_kind": "non_capability", + "confidence": "non_capability", + "rationale": "Applicability + transition dates (old Directive 2006/42/EC -> Regulation 2023/1230). This drives the SCOPE engine, not a capability. RS-004 part C (reg-ID/scope wiring) is the right home.", + "owner_decision": "Reasoning/scope-engine, not a capability." + }, + { + "obligation_id": "specific_machine_types", + "subdomain": "specific_machinery", + "link_kind": "composite", + "confidence": "proposed", + "rationale": "Machine-type-specific EHSR (e.g. lifting, portable, wood/food machinery). Resolves to MULTIPLE physical_safety capabilities depending on machine type — model as a type-conditional set, not one control.", + "owner_decision": "Execution: expand per machine-type once the physical_safety capabilities are minted." + } + ], + "summary": { + "obligations_total": 31, + "cyber_safety_bridges_high_confidence": 2, + "cyber_safety_bridges_needs_check": 1, + "cross_regulation_bridges": 2, + "physical_safety_candidates": 7, + "process_candidates": 13, + "non_capability_flags": 3, + "composite": 1, + "headline": "The 2 high-confidence cyber-safety bridges are immediately wirable to existing CRA-core obligations + capabilities (the CRA<->MaschinenVO convergence USP). Everything else is safety-expert input for Execution to mint and Legal-KG to ratify." + } +}