Compare commits
123 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1d65d99d5f | |||
| f2d445b891 | |||
| 08086ee75f | |||
| 1e5aaf7103 | |||
| af11d21f6e | |||
| e2c74fd243 | |||
| 8ed99c255d | |||
| 3389fa3e7a | |||
| 79abf23ea8 | |||
| d5925e57af | |||
| 1877829b1d | |||
| 866889b453 | |||
| 9760dca443 | |||
| e5e7b825af | |||
| 4818fc51c2 | |||
| f0da86ca19 | |||
| 867f8c3854 | |||
| 26a8518107 | |||
| 807a7002b2 | |||
| 5beb5a319a | |||
| 239702fdca | |||
| d1a5fc7205 | |||
| 7df15010ff | |||
| e54f3cde94 | |||
| 77459d06d6 | |||
| 3202e555ab | |||
| 978052b5a2 | |||
| 19931208a9 | |||
| c39787ad96 | |||
| b5b6cdddb3 | |||
| a4123ace71 | |||
| 3bb48f2147 | |||
| c2c8f7e424 | |||
| b70c1b7c37 | |||
| 9c33582412 | |||
| 23d977e26b | |||
| 88b83d4daf | |||
| 98d616d82b | |||
| 59b7006e5a | |||
| 2d2cb2a244 | |||
| 02c9fdb18e | |||
| 3ba90f49cf | |||
| 009083882a | |||
| a98076196b | |||
| afe5a98474 | |||
| 80f2e2f619 | |||
| 897e9464a7 | |||
| c160bb8291 | |||
| a2332fb13d | |||
| 90c3fe16b5 | |||
| e0d9816c99 | |||
| fbbd0957bd | |||
| 2805256c33 | |||
| cefacb87af | |||
| d0575d286f | |||
| 80bf1993e0 | |||
| 3c6e2a2acc | |||
| dbf7b9b587 | |||
| 5cba0504df | |||
| 77d6bc5551 | |||
| d196ad1cab | |||
| b71771e52e | |||
| 256bb0607d | |||
| ff9a66fb72 | |||
| 363c76d274 | |||
| dfb2c6dfdb | |||
| 16d6ad4122 | |||
| 3856bb3a4f | |||
| 0b962b41fa | |||
| b6c400902e | |||
| 98f67e75d9 | |||
| f652e2d4ed | |||
| ecae5bc7f1 | |||
| 23a6f02ec2 | |||
| 4a7412e4f2 | |||
| 0cb224a7f1 | |||
| d44f3672be | |||
| c98500c303 | |||
| 4efbfa45c4 | |||
| 86a783e72f | |||
| 1054facffa | |||
| 18f5d0cb05 | |||
| a2403eaed9 | |||
| 1a9439d013 | |||
| c737e1ad7d | |||
| 9c02c2c4a2 | |||
| c4e9ca8f4d | |||
| aa99111a87 | |||
| 0b0d262462 | |||
| 07e392913f | |||
| d51bcd77c7 | |||
| b6cfc0a503 | |||
| 1e1689f1f2 | |||
| 78f0ffa9de | |||
| 50d88d611d | |||
| cfafa31ea2 | |||
| ffff9bb592 | |||
| a0f72fc39b | |||
| 5fde7690a5 | |||
| 66be23f0c4 | |||
| caa9b8b609 | |||
| f78e03bd0a | |||
| 5412864705 | |||
| 0da093c046 | |||
| 3199d0d90e | |||
| 4bfd552da7 | |||
| cb18eac7ec | |||
| bea8559f78 | |||
| 81f8b56b48 | |||
| db2efe9f52 | |||
| 77de7e794c | |||
| fb4e14d9b9 | |||
| 5e735e9e56 | |||
| 24fdde89c6 | |||
| f3d3255de1 | |||
| fe21c2f487 | |||
| e4695cf289 | |||
| d72dcbacfb | |||
| 8a51db92ed | |||
| 16371f2909 | |||
| c7339e68df | |||
| 06efb9e61b | |||
| aaacec087c |
@@ -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<string, string>
|
||||
}
|
||||
|
||||
const PROXY = '/api/sdk/v1/compliance/onboarding'
|
||||
|
||||
function Chips({ items, tone }: { items: string[]; tone: string }) {
|
||||
if (!items.length) return <span className="text-gray-400 text-sm">—</span>
|
||||
return (
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{items.map(c => <span key={c} className={`px-2.5 py-1 rounded-full text-xs font-medium ${tone}`}>{c}</span>)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function Section({ title, hint, children }: { title: string; hint?: string; children: React.ReactNode }) {
|
||||
return (
|
||||
<div className="bg-white rounded-xl border border-gray-200 p-5">
|
||||
<h3 className="font-semibold text-gray-900">{title}</h3>
|
||||
{hint && <p className="text-xs text-gray-500 mt-0.5 mb-2">{hint}</p>}
|
||||
<div className="mt-2">{children}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default function OnboardingAdvisorPage() {
|
||||
const [targets, setTargets] = useState<string[]>([])
|
||||
const [company, setCompany] = useState('Beispiel Maschinenbau')
|
||||
const [industry, setIndustry] = useState('machine_builder')
|
||||
const [certs, setCerts] = useState<string[]>(['ISO27001', 'ISO9001'])
|
||||
const [target, setTarget] = useState('CRA')
|
||||
const [findings, setFindings] = useState<string[]>(['cyclonedx_found', 'github_actions_ci', 'requires_sbom'])
|
||||
const [knownEvidence, setKnownEvidence] = useState('CE-Prozess')
|
||||
const [result, setResult] = useState<AdvisorResponse | null>(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 (
|
||||
<div className="min-h-screen bg-gray-50 py-8">
|
||||
<div className="max-w-5xl mx-auto px-4">
|
||||
<h1 className="text-3xl font-bold text-gray-900">ETO / Onboarding-Advisor</h1>
|
||||
<p className="text-gray-600 mt-2 mb-6">
|
||||
Zertifikate + Ziel + Scanner-Signale → Silent Pass → Capability-Delta + nächste beste Fragen.
|
||||
Welt-1: ein Zertifikat <em>legt nahe</em>, beweist nichts (Verifikation erforderlich).
|
||||
</p>
|
||||
|
||||
<div className="grid md:grid-cols-2 gap-4 mb-6">
|
||||
<Section title="Unternehmen & Ziel">
|
||||
<label className="block text-sm text-gray-600">Unternehmen
|
||||
<input value={company} onChange={e => setCompany(e.target.value)} className="mt-1 w-full border rounded-lg px-3 py-2" /></label>
|
||||
<label className="block text-sm text-gray-600 mt-3">Branche
|
||||
<input value={industry} onChange={e => setIndustry(e.target.value)} className="mt-1 w-full border rounded-lg px-3 py-2" /></label>
|
||||
<label className="block text-sm text-gray-600 mt-3">Ziel
|
||||
<select value={target} onChange={e => setTarget(e.target.value)} className="mt-1 w-full border rounded-lg px-3 py-2">
|
||||
{(targets.length ? targets : ['CRA']).map(t => <option key={t} value={t}>{t}</option>)}
|
||||
</select></label>
|
||||
<label className="block text-sm text-gray-600 mt-3">Vorhandene Nachweise (kommagetrennt)
|
||||
<input value={knownEvidence} onChange={e => setKnownEvidence(e.target.value)} className="mt-1 w-full border rounded-lg px-3 py-2" /></label>
|
||||
</Section>
|
||||
|
||||
<Section title="Zertifizierungen">
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{CERTS.map(c => (
|
||||
<button key={c} onClick={() => toggle(certs, setCerts, c)}
|
||||
className={`px-3 py-1.5 rounded-lg text-sm border ${certs.includes(c) ? 'bg-blue-600 text-white border-blue-600' : 'bg-white text-gray-700 border-gray-300'}`}>{c}</button>
|
||||
))}
|
||||
</div>
|
||||
</Section>
|
||||
</div>
|
||||
|
||||
<Section title="Scanner-Signale (Silent Pass)" hint="observation = gesehen · partial = Indikation · requirement = gefordert (≠ vorhanden)">
|
||||
<div className="grid sm:grid-cols-2 gap-2">
|
||||
{FINDINGS.map(f => (
|
||||
<label key={f.signal_id} className="flex items-center gap-2 text-sm text-gray-700">
|
||||
<input type="checkbox" checked={findings.includes(f.signal_id)} onChange={() => toggle(findings, setFindings, f.signal_id)} />
|
||||
<span>{f.label}</span>
|
||||
<span className={`ml-auto text-[10px] px-1.5 py-0.5 rounded ${f.kind === 'requirement' ? 'bg-purple-100 text-purple-700' : f.kind === 'partial' ? 'bg-amber-100 text-amber-700' : 'bg-emerald-100 text-emerald-700'}`}>{f.kind}</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<button onClick={run} disabled={loading || !certs.length}
|
||||
className="mt-6 w-full py-3 bg-blue-600 text-white rounded-xl font-medium hover:bg-blue-700 disabled:opacity-50">
|
||||
{loading ? 'Analysiere…' : 'Advisor starten'}
|
||||
</button>
|
||||
|
||||
{error && <div className="mt-6 bg-red-50 border border-red-200 rounded-lg p-4 text-red-700 text-sm whitespace-pre-wrap">{error}</div>}
|
||||
|
||||
{result && (
|
||||
<div className="mt-8 space-y-4">
|
||||
<div className="bg-blue-600 text-white rounded-xl p-5">
|
||||
<div className="text-lg font-semibold">{result.headline}</div>
|
||||
<div className="text-blue-100 text-sm mt-1">{result.silent_intake_summary}</div>
|
||||
</div>
|
||||
<div className="grid md:grid-cols-2 gap-4">
|
||||
<Section title="Automatisch erkannt" hint="konkrete Artefakte – nicht mehr gefragt"><Chips items={result.auto_detected.map(lbl)} tone="bg-emerald-100 text-emerald-800" /></Section>
|
||||
<Section title="Indikationen" hint="erhöht Annahmestärke – trotzdem gefragt"><Chips items={result.indications.map(lbl)} tone="bg-amber-100 text-amber-800" /></Section>
|
||||
</div>
|
||||
<Section title="Nächste beste Fragen" hint="max 5, jede erklärt sich selbst">
|
||||
{result.top_5_questions.length ? (
|
||||
<ol className="space-y-3">
|
||||
{result.top_5_questions.map((q, i) => (
|
||||
<li key={q.capability_id} className="border-l-2 border-blue-300 pl-3">
|
||||
<div className="font-medium text-gray-900">{i + 1}. {lbl(q.capability_id)}</div>
|
||||
<div className="text-sm text-gray-600">{q.why}</div>
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
) : <span className="text-gray-400 text-sm">—</span>}
|
||||
</Section>
|
||||
<div className="grid md:grid-cols-2 gap-4">
|
||||
<Section title="Wahrscheinlich abgedeckt (Welt-1)" hint="Zertifikat legt nahe – Verifikation erforderlich">
|
||||
{result.inferred_assumptions.length ? result.inferred_assumptions.map(a => (
|
||||
<div key={a.certification} className="mb-2"><span className="font-medium">{a.certification}</span>: {a.capabilities.map(lbl).join(', ')}</div>
|
||||
)) : <span className="text-gray-400 text-sm">—</span>}
|
||||
</Section>
|
||||
<Section title="Nicht relevant" hint="relevance(evidence, target) = 0">
|
||||
{result.rejected_assumptions.length ? result.rejected_assumptions.map((a, i) => (
|
||||
<div key={i} className="mb-1 text-sm text-gray-700">{a.statement}</div>
|
||||
)) : <span className="text-gray-400 text-sm">—</span>}
|
||||
</Section>
|
||||
</div>
|
||||
<div className="grid md:grid-cols-2 gap-4">
|
||||
<Section title="Offene Lücken (Delta)"><Chips items={result.capability_delta.map(lbl)} tone="bg-gray-100 text-gray-700" /></Section>
|
||||
<Section title="Geforderte Nachweise"><Chips items={result.evidence_requests} tone="bg-gray-100 text-gray-700" /></Section>
|
||||
</div>
|
||||
<Section title="Vollständigkeit" hint={result.unsupported_domains.length ? `nicht abgedeckt: ${result.unsupported_domains.join(', ')}` : undefined}>
|
||||
<span className="text-sm text-gray-700">{result.completeness_summary || '—'}</span>
|
||||
</Section>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -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')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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<ScoredPassage[]> {
|
||||
/**
|
||||
* 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<string> {
|
||||
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 []
|
||||
if (res.ok) {
|
||||
const data = await res.json()
|
||||
return mapSdkResults(data.results)
|
||||
} catch {
|
||||
return []
|
||||
passages = mapSdkResults(data.results)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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<string> {
|
||||
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)
|
||||
} catch {
|
||||
// graceful: keine Verbindung -> Antwort ohne RAG-Kontext
|
||||
}
|
||||
// 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')
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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")
|
||||
{
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
@@ -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"}
|
||||
]
|
||||
}
|
||||
@@ -28,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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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, ";"))
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,6 +51,14 @@ 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,
|
||||
@@ -54,6 +68,8 @@ func NewLegalRAGClient() *LegalRAGClient {
|
||||
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,
|
||||
|
||||
@@ -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}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -78,6 +78,7 @@ _ROUTER_MODULES = [
|
||||
"template_rule_routes",
|
||||
"specialist_agent_routes",
|
||||
"reasoning_routes",
|
||||
"onboarding_routes",
|
||||
]
|
||||
|
||||
_loaded_count = 0
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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"])
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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."""
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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))
|
||||
@@ -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."""
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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",
|
||||
]
|
||||
@@ -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,
|
||||
)
|
||||
@@ -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
|
||||
@@ -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",
|
||||
]
|
||||
@@ -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)
|
||||
@@ -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 = ""
|
||||
@@ -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",
|
||||
]
|
||||
@@ -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,
|
||||
)
|
||||
@@ -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 = ""
|
||||
@@ -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",
|
||||
]
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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",
|
||||
]
|
||||
@@ -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
|
||||
@@ -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)}
|
||||
@@ -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]
|
||||
@@ -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]
|
||||
@@ -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"
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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",
|
||||
]
|
||||
@@ -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)
|
||||
@@ -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 = ""
|
||||
@@ -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",
|
||||
]
|
||||
@@ -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
|
||||
]
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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",
|
||||
]
|
||||
@@ -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,
|
||||
)
|
||||
@@ -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
|
||||
)
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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."}
|
||||
@@ -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]}
|
||||
@@ -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]
|
||||
@@ -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`.
|
||||
+66
@@ -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).
|
||||
+58
@@ -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).
|
||||
+64
@@ -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).
|
||||
+69
@@ -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).
|
||||
+63
@@ -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).
|
||||
+71
@@ -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).
|
||||
@@ -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).
|
||||
@@ -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.
|
||||
@@ -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"
|
||||
@@ -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}
|
||||
@@ -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]}
|
||||
@@ -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.
|
||||
@@ -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).
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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]}
|
||||
@@ -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."
|
||||
@@ -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."
|
||||
@@ -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.
|
||||
@@ -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_<from>_to_<to>_v<n>.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`.
|
||||
+191
@@ -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)."
|
||||
+82
@@ -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."
|
||||
+118
@@ -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."
|
||||
+84
@@ -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."
|
||||
+232
@@ -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."
|
||||
+95
@@ -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."
|
||||
@@ -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.
|
||||
@@ -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]}
|
||||
@@ -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)"),
|
||||
])
|
||||
@@ -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.
|
||||
|
||||
@@ -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))
|
||||
@@ -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.
|
||||
|
||||
@@ -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))
|
||||
@@ -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.
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user