feat(iace): add hazard-matching-engine with component library, tag system, and pattern engine
All checks were successful
CI/CD / go-lint (push) Has been skipped
CI/CD / python-lint (push) Has been skipped
CI/CD / nodejs-lint (push) Has been skipped
CI/CD / test-go-ai-compliance (push) Successful in 44s
CI/CD / test-python-backend-compliance (push) Successful in 33s
CI/CD / test-python-document-crawler (push) Successful in 22s
CI/CD / test-python-dsms-gateway (push) Successful in 19s
CI/CD / validate-canonical-controls (push) Successful in 13s
CI/CD / Deploy (push) Successful in 4s

Implements Phases 1-4 of the IACE Hazard-Matching-Engine:
- 120 machine components (C001-C120) in 11 categories
- 20 energy sources (EN01-EN20)
- ~85 tag taxonomy across 5 domains
- 44 hazard patterns with AND/NOT matching logic
- Pattern engine with tag resolution and confidence scoring
- 8 new API endpoints (component-library, energy-sources, tags, patterns, match/apply)
- Completeness gate G09 for pattern matching
- 320 tests passing (36 new)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Benjamin Admin
2026-03-16 08:50:11 +01:00
parent c7651796c9
commit 3b2006ebce
21 changed files with 4993 additions and 41 deletions

View File

@@ -223,13 +223,13 @@ def _classify_regulation(regulation_code: str) -> dict:
DOMAIN_KEYWORDS = {
"AUTH": ["authentication", "login", "password", "credential", "mfa", "2fa",
"session", "token", "oauth", "identity", "authentifizierung", "anmeldung"],
"CRYPT": ["encryption", "cryptography", "tls", "ssl", "certificate", "hashing",
"aes", "rsa", "verschlüsselung", "kryptographie", "zertifikat"],
"CRYP": ["encryption", "cryptography", "tls", "ssl", "certificate", "hashing",
"aes", "rsa", "verschlüsselung", "kryptographie", "cipher", "schlüssel"],
"NET": ["network", "firewall", "dns", "vpn", "proxy", "segmentation",
"netzwerk", "routing", "port", "intrusion"],
"DATA": ["data protection", "privacy", "personal data", "datenschutz",
"personenbezogen", "dsgvo", "gdpr", "löschung", "verarbeitung"],
"LOG": ["logging", "monitoring", "audit", "siem", "alert", "anomaly",
"LOG": ["logging", "monitoring", "audit trail", "siem", "alert", "anomaly",
"protokollierung", "überwachung"],
"ACC": ["access control", "authorization", "rbac", "permission", "privilege",
"zugriffskontrolle", "berechtigung", "autorisierung"],
@@ -241,12 +241,30 @@ DOMAIN_KEYWORDS = {
"ki", "künstliche intelligenz", "algorithmus", "training"],
"COMP": ["compliance", "audit", "regulation", "standard", "certification",
"konformität", "prüfung", "zertifizierung"],
"GOV": ["behörde", "verwaltung", "öffentlich", "register", "gewerberegister",
"handelsregister", "meldepflicht", "aufsicht", "genehmigung", "bescheid",
"verwaltungsakt", "ordnungswidrig", "bußgeld", "staat", "ministerium",
"bundesamt", "landesamt", "kommune", "gebietskörperschaft"],
"LAB": ["arbeitnehmer", "arbeitgeber", "arbeitsschutz", "arbeitszeit", "betriebsrat",
"kündigung", "beschäftigung", "mindestlohn", "arbeitsvertrag", "betriebsverfassung",
"arbeitsrecht", "arbeitsstätte", "gefährdungsbeurteilung", "unterweisung"],
"FIN": ["finanz", "bankwesen", "zahlungsverkehr", "geldwäsche", "bilanz", "rechnungslegung",
"buchführung", "jahresabschluss", "steuererklärung", "kapitalmarkt", "wertpapier",
"kreditinstitut", "finanzdienstleistung", "bankenaufsicht", "bafin"],
"TRD": ["handelsrecht", "gewerbeordnung", "gewerbe", "handwerk", "gewerbeuntersagung",
"gewerbebetrieb", "handelsgesetzbuch", "handelsregister", "kaufmann",
"unternehmer", "wettbewerb", "verbraucherschutz", "produktsicherheit"],
"ENV": ["umweltschutz", "emission", "abfall", "immission", "gewässerschutz",
"naturschutz", "umweltverträglichkeit", "klimaschutz", "nachhaltigkeit",
"entsorgung", "recycling", "umweltrecht"],
"HLT": ["gesundheit", "medizinprodukt", "arzneimittel", "patient", "krankenhaus",
"hygiene", "infektionsschutz", "medizin", "pflege", "therapie"],
}
CATEGORY_KEYWORDS = {
"encryption": ["encryption", "cryptography", "tls", "ssl", "certificate", "hashing",
"aes", "rsa", "verschlüsselung", "kryptographie", "zertifikat", "cipher"],
"aes", "rsa", "verschlüsselung", "kryptographie", "cipher", "schlüssel"],
"authentication": ["authentication", "login", "password", "credential", "mfa", "2fa",
"session", "oauth", "authentifizierung", "anmeldung", "passwort"],
"network": ["network", "firewall", "dns", "vpn", "proxy", "segmentation",
@@ -278,6 +296,20 @@ CATEGORY_KEYWORDS = {
"plattform", "geräte"],
"identity": ["identity", "iam", "directory", "ldap", "sso", "provisioning",
"identität", "identitätsmanagement", "benutzerverzeichnis"],
"public_administration": ["behörde", "verwaltung", "öffentlich", "register", "gewerberegister",
"handelsregister", "meldepflicht", "aufsicht", "genehmigung", "bescheid",
"verwaltungsakt", "ordnungswidrig", "bußgeld", "amt"],
"labor_law": ["arbeitnehmer", "arbeitgeber", "arbeitsschutz", "arbeitszeit", "betriebsrat",
"kündigung", "beschäftigung", "mindestlohn", "arbeitsvertrag", "betriebsverfassung"],
"finance": ["finanz", "bankwesen", "zahlungsverkehr", "geldwäsche", "bilanz", "rechnungslegung",
"buchführung", "jahresabschluss", "kapitalmarkt", "wertpapier", "bafin"],
"trade_regulation": ["gewerbeordnung", "gewerbe", "handwerk", "gewerbeuntersagung",
"gewerbebetrieb", "handelsrecht", "kaufmann", "wettbewerb",
"verbraucherschutz", "produktsicherheit"],
"environmental": ["umweltschutz", "emission", "abfall", "immission", "gewässerschutz",
"naturschutz", "klimaschutz", "nachhaltigkeit", "entsorgung"],
"health": ["gesundheit", "medizinprodukt", "arzneimittel", "patient", "krankenhaus",
"hygiene", "infektionsschutz", "pflege"],
}
VERIFICATION_KEYWORDS = {
@@ -372,7 +404,8 @@ class GeneratedControl:
generation_strategy: str = "ungrouped" # ungrouped | document_grouped
# Classification fields
verification_method: Optional[str] = None # code_review, document, tool, hybrid
category: Optional[str] = None # one of 17 categories
category: Optional[str] = None # one of 22 categories
target_audience: Optional[list] = None # e.g. ["unternehmen", "behoerden", "entwickler"]
@dataclass
@@ -705,9 +738,11 @@ class ControlGeneratorPipeline:
page = 0
collection_total = 0
collection_new = 0
seen_offsets: set[str] = set() # Detect scroll loops
max_pages = 1000 # Safety limit: 1000 pages × 200 = 200K chunks max per collection
prev_chunk_count = -1 # Track stalls (same count means no progress)
stall_count = 0
while True:
while page < max_pages:
chunks, next_offset = await self.rag.scroll(
collection=collection,
offset=offset,
@@ -747,17 +782,30 @@ class ControlGeneratorPipeline:
# Stop conditions
if not next_offset:
break
# Detect infinite scroll loops (Qdrant mixed ID types)
if next_offset in seen_offsets:
logger.warning(
"Scroll loop detected in %s at offset %s (page %d) — stopping",
collection, next_offset, page,
)
break
seen_offsets.add(next_offset)
# Detect stalls: if no NEW unique chunks found for several pages,
# we've likely cycled through all chunks in this collection.
# (Safer than offset dedup which breaks with mixed Qdrant ID types)
if collection_new == prev_chunk_count:
stall_count += 1
if stall_count >= 5:
logger.warning(
"Scroll stalled in %s at page %d — no new unique chunks for 5 pages (%d total, %d new) — stopping",
collection, page, collection_total, collection_new,
)
break
else:
stall_count = 0
prev_chunk_count = collection_new
offset = next_offset
if page >= max_pages:
logger.warning(
"Collection %s: reached max_pages limit (%d). %d chunks scrolled.",
collection, max_pages, collection_total,
)
logger.info(
"Collection %s: %d total chunks scrolled, %d new unprocessed",
collection, collection_total, collection_new,
@@ -823,7 +871,9 @@ Quelle: {chunk.regulation_name} ({chunk.regulation_code}), {chunk.article}"""
control.license_rule = 1
control.source_original_text = chunk.text
control.source_citation = {
"source": f"{chunk.regulation_name} {chunk.article or ''}".strip(),
"source": chunk.regulation_name,
"article": chunk.article or "",
"paragraph": chunk.paragraph or "",
"license": license_info.get("license", ""),
"url": chunk.source_url or "",
}
@@ -835,6 +885,7 @@ Quelle: {chunk.regulation_name} ({chunk.regulation_code}), {chunk.article}"""
"license_rule": 1,
"source_regulation": chunk.regulation_code,
"source_article": chunk.article,
"source_paragraph": chunk.paragraph,
}
return control
@@ -873,7 +924,9 @@ Quelle: {chunk.regulation_name}, {chunk.article}"""
control.license_rule = 2
control.source_original_text = chunk.text
control.source_citation = {
"source": f"{chunk.regulation_name} {chunk.article or ''}".strip(),
"source": chunk.regulation_name,
"article": chunk.article or "",
"paragraph": chunk.paragraph or "",
"license": license_info.get("license", ""),
"license_notice": attribution,
"url": chunk.source_url or "",
@@ -886,6 +939,7 @@ Quelle: {chunk.regulation_name}, {chunk.article}"""
"license_rule": 2,
"source_regulation": chunk.regulation_code,
"source_article": chunk.article,
"source_paragraph": chunk.paragraph,
}
return control
@@ -913,7 +967,9 @@ Gib JSON zurück mit diesen Feldern:
- test_procedure: Liste von Prüfschritten (Strings)
- evidence: Liste von Nachweisdokumenten (Strings)
- severity: low/medium/high/critical
- tags: Liste von Tags (eigene Begriffe)"""
- tags: Liste von Tags (eigene Begriffe)
- domain: Fachgebiet als Kuerzel (AUTH, CRYP, NET, DATA, LOG, ACC, SEC, INC, AI, COMP, GOV, LAB, FIN, TRD, ENV, HLT)
- target_audience: Liste der Zielgruppen (z.B. "unternehmen", "behoerden", "entwickler", "datenschutzbeauftragte", "geschaeftsfuehrung", "it-abteilung", "rechtsabteilung", "compliance-officer", "personalwesen", "oeffentlicher_dienst")"""
raw = await _llm_chat(prompt, REFORM_SYSTEM_PROMPT)
data = _parse_llm_json(raw)
@@ -989,6 +1045,8 @@ Gib ein JSON-Array zurueck mit GENAU {len(chunks)} Objekten. Jedes Objekt hat di
- evidence: Liste von Nachweisdokumenten (Strings, Deutsch)
- severity: low/medium/high/critical
- tags: Liste von Tags
- domain: Fachgebiet als Kuerzel (AUTH=Authentifizierung, CRYP=Kryptographie, NET=Netzwerk, DATA=Datenschutz, LOG=Logging, ACC=Zugriffskontrolle, SEC=IT-Sicherheit, INC=Vorfallmanagement, AI=KI, COMP=Compliance, GOV=Behoerden/Verwaltung, LAB=Arbeitsrecht, FIN=Finanzregulierung, TRD=Gewerbe/Handelsrecht, ENV=Umwelt, HLT=Gesundheit)
- target_audience: Liste der Zielgruppen fuer die dieses Control relevant ist. Moegliche Werte: "unternehmen", "behoerden", "entwickler", "datenschutzbeauftragte", "geschaeftsfuehrung", "it-abteilung", "rechtsabteilung", "compliance-officer", "personalwesen", "einkauf", "produktion", "vertrieb", "gesundheitswesen", "finanzwesen", "oeffentlicher_dienst"
{joined}"""
@@ -1016,7 +1074,9 @@ Gib ein JSON-Array zurueck mit GENAU {len(chunks)} Objekten. Jedes Objekt hat di
if lic["rule"] in (1, 2):
control.source_original_text = chunk.text
control.source_citation = {
"source": f"{chunk.regulation_name} {chunk.article or ''}".strip(),
"source": chunk.regulation_name,
"article": chunk.article or "",
"paragraph": chunk.paragraph or "",
"license": lic.get("license", ""),
"license_notice": lic.get("attribution", ""),
"url": chunk.source_url or "",
@@ -1030,6 +1090,7 @@ Gib ein JSON-Array zurueck mit GENAU {len(chunks)} Objekten. Jedes Objekt hat di
"license_rule": lic["rule"],
"source_regulation": chunk.regulation_code,
"source_article": chunk.article,
"source_paragraph": chunk.paragraph,
"batch_size": len(chunks),
"document_grouped": same_doc,
}
@@ -1071,6 +1132,8 @@ Gib ein JSON-Array zurueck mit GENAU {len(chunks)} Objekten. Jedes Objekt hat di
- evidence: Liste von Nachweisdokumenten (Strings)
- severity: low/medium/high/critical
- tags: Liste von Tags (eigene Begriffe)
- domain: Fachgebiet als Kuerzel (AUTH=Authentifizierung, CRYP=Kryptographie, NET=Netzwerk, DATA=Datenschutz, LOG=Logging, ACC=Zugriffskontrolle, SEC=IT-Sicherheit, INC=Vorfallmanagement, AI=KI, COMP=Compliance, GOV=Behoerden/Verwaltung, LAB=Arbeitsrecht, FIN=Finanzregulierung, TRD=Gewerbe/Handelsrecht, ENV=Umwelt, HLT=Gesundheit)
- target_audience: Liste der Zielgruppen (z.B. "unternehmen", "behoerden", "entwickler", "datenschutzbeauftragte", "geschaeftsfuehrung", "it-abteilung", "rechtsabteilung", "compliance-officer", "personalwesen", "einkauf", "produktion", "gesundheitswesen", "finanzwesen", "oeffentlicher_dienst")
{joined}"""
@@ -1182,8 +1245,10 @@ Gib ein JSON-Array zurueck mit GENAU {len(chunks)} Objekten. Jedes Objekt hat di
else:
control.release_state = "needs_review"
# Control ID
domain = config.domain or _detect_domain(control.objective)
# Control ID — prefer LLM-assigned domain over keyword detection
domain = (control.generation_metadata.get("_effective_domain")
or config.domain
or _detect_domain(control.objective))
control.control_id = self._generate_control_id(domain, self.db)
control.generation_metadata["job_id"] = job_id
@@ -1270,7 +1335,21 @@ Gib ein JSON-Array zurueck mit GENAU {len(chunks)} Objekten. Jedes Objekt hat di
if isinstance(tags, str):
tags = [t.strip() for t in tags.split(",")]
return GeneratedControl(
# Use LLM-provided domain if available, fallback to keyword-detected domain
llm_domain = data.get("domain")
valid_domains = {"AUTH", "CRYP", "NET", "DATA", "LOG", "ACC", "SEC", "INC",
"AI", "COMP", "GOV", "LAB", "FIN", "TRD", "ENV", "HLT"}
if llm_domain and llm_domain.upper() in valid_domains:
domain = llm_domain.upper()
# Parse target_audience from LLM response
target_audience = data.get("target_audience")
if isinstance(target_audience, str):
target_audience = [t.strip() for t in target_audience.split(",")]
if not isinstance(target_audience, list):
target_audience = None
control = GeneratedControl(
title=str(data.get("title", "Untitled Control"))[:255],
objective=str(data.get("objective", "")),
rationale=str(data.get("rationale", "")),
@@ -1282,7 +1361,11 @@ Gib ein JSON-Array zurueck mit GENAU {len(chunks)} Objekten. Jedes Objekt hat di
risk_score=min(10.0, max(0.0, float(data.get("risk_score", 5.0)))),
implementation_effort=data.get("implementation_effort", "m") if data.get("implementation_effort") in ("s", "m", "l", "xl") else "m",
tags=tags[:20],
target_audience=target_audience,
)
# Store effective domain for later control_id generation
control.generation_metadata["_effective_domain"] = domain
return control
def _fallback_control(self, chunk: RAGSearchResult) -> GeneratedControl:
"""Create a minimal control when LLM parsing fails."""
@@ -1393,7 +1476,8 @@ Gib ein JSON-Array zurueck mit GENAU {len(chunks)} Objekten. Jedes Objekt hat di
open_anchors, release_state, tags,
license_rule, source_original_text, source_citation,
customer_visible, generation_metadata,
verification_method, category, generation_strategy
verification_method, category, generation_strategy,
target_audience
) VALUES (
:framework_id, :control_id, :title, :objective, :rationale,
:scope, :requirements, :test_procedure, :evidence,
@@ -1401,7 +1485,8 @@ Gib ein JSON-Array zurueck mit GENAU {len(chunks)} Objekten. Jedes Objekt hat di
:open_anchors, :release_state, :tags,
:license_rule, :source_original_text, :source_citation,
:customer_visible, :generation_metadata,
:verification_method, :category, :generation_strategy
:verification_method, :category, :generation_strategy,
:target_audience
)
ON CONFLICT (framework_id, control_id) DO NOTHING
RETURNING id
@@ -1430,6 +1515,7 @@ Gib ein JSON-Array zurueck mit GENAU {len(chunks)} Objekten. Jedes Objekt hat di
"verification_method": control.verification_method,
"category": control.category,
"generation_strategy": control.generation_strategy,
"target_audience": json.dumps(control.target_audience) if control.target_audience else None,
},
)
self.db.commit()