feat(controls): 5 neue Use Cases + Machinery-Fix + Korpus-/Lizenz-Übersicht
CI / detect-changes (push) Successful in 14s
CI / branch-name (push) Has been skipped
CI / guardrail-integrity (push) Has been skipped
CI / secret-scan (push) Has been skipped
CI / dep-audit (push) Has been skipped
CI / sbom-scan (push) Has been skipped
CI / build-sha-integrity (push) Successful in 11s
CI / validate-canonical-controls (push) Failing after 5s
CI / loc-budget (push) Successful in 22s
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / test-go (push) Successful in 1m11s
CI / nodejs-lint (push) Has been skipped
CI / nodejs-build (push) Successful in 3m41s
CI / iace-gt-coverage (push) Failing after 5s
CI / test-python-backend (push) Failing after 5s
CI / test-python-document-crawler (push) Has been skipped
CI / test-python-dsms-gateway (push) Has been skipped

- Registry: arbeitsrecht, gesellschaftsrecht, insolvenzrecht, csrd, bafin_it
  + Mapper-Regeln für zuvor ungemappte Quell-Gesetze, Machinery-Guide 2006/42
  -> maschinen. Jetzt 43 Use Cases (Achse 1 / license 1+2 vollständig).
- corpus_overview Service + GET /v1/controls/corpus: Quell-Dokumente mit
  Lizenz-Tier + atom-Count + Use-Case + kuratiertem Lizenz-Katalog.
- list_use_cases trägt atom_classification-Counts (atom_total/atom_relevant).
- Frontend /sdk/coverage: Use-Case-Übersicht + Korpus-Dokumente + Lizenz-Katalog.
- Tests: registry-Mappings (neue Domänen), corpus tier-labels, coverage-helpers.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Benjamin Admin
2026-06-14 21:49:22 +02:00
parent 60f988f3cb
commit 00f304fed9
9 changed files with 585 additions and 2 deletions
@@ -0,0 +1,55 @@
import { describe, it, expect } from 'vitest'
import {
licenseTierBadgeClass,
commercialBadgeClass,
groupUseCases,
type UseCaseRow,
} from './_helpers'
const uc = (over: Partial<UseCaseRow>): UseCaseRow => ({
key: 'x',
label: 'X',
group: 'security',
regulations: [],
verification_methods: [],
mapped_controls: 0,
atom_total: 0,
atom_relevant: 0,
...over,
})
describe('coverage helpers', () => {
it('license tier badge classes', () => {
expect(licenseTierBadgeClass(1)).toContain('green')
expect(licenseTierBadgeClass(2)).toContain('blue')
expect(licenseTierBadgeClass(3)).toContain('amber')
expect(licenseTierBadgeClass(null)).toContain('gray')
})
it('commercial-use badge classes', () => {
expect(commercialBadgeClass('allowed')).toContain('green')
expect(commercialBadgeClass('restricted')).toContain('amber')
expect(commercialBadgeClass('prohibited')).toContain('red')
expect(commercialBadgeClass(null)).toContain('gray')
})
it('groups use-cases in stable order and sorts by relevant desc', () => {
const groups = groupUseCases([
uc({ key: 'a', group: 'security', atom_relevant: 5 }),
uc({ key: 'b', group: 'security', atom_relevant: 15 }),
uc({ key: 'c', group: 'document', atom_relevant: 1 }),
])
expect(groups[0].group).toBe('document')
expect(groups[1].group).toBe('security')
expect(groups[1].rows[0].key).toBe('b')
expect(groups[1].rows[1].key).toBe('a')
})
it('appends unknown groups after the known order', () => {
const groups = groupUseCases([
uc({ key: 'z', group: 'mystery', atom_relevant: 9 }),
uc({ key: 'd', group: 'document', atom_relevant: 2 }),
])
expect(groups.map((g) => g.group)).toEqual(['document', 'mystery'])
})
})
@@ -0,0 +1,105 @@
// Pure helpers for the Coverage page (#74). Kept separate so they are unit-testable
// without rendering the server component.
export interface UseCaseRow {
key: string
label: string
group: string
regulations: string[]
verification_methods: string[]
mapped_controls: number
atom_total: number
atom_relevant: number
}
export interface CorpusDoc {
source_regulation: string
license_rule: number | null
license_tier: string
atom_count: number
use_case: string | null
}
export interface LicenseSummaryRow {
license_rule: number | null
label: string
atom_count: number
}
export interface LicenseCatalogEntry {
source_id: string
title: string
publisher: string | null
url: string | null
version: string | null
license_id: string | null
license_name: string | null
commercial_use: string | null
ship_in_product: boolean | null
terms_url: string | null
}
export interface CorpusOverview {
license_summary: LicenseSummaryRow[]
documents: CorpusDoc[]
license_catalog: LicenseCatalogEntry[]
totals: { documents: number; catalog_sources: number }
}
export const USE_CASE_GROUP_LABELS: Record<string, string> = {
document: 'Dokument-Compliance',
security: 'Security',
cross_cutting: 'Querschnitt',
product: 'Produkt / Sektor',
}
export function licenseTierBadgeClass(rule: number | null): string {
switch (rule) {
case 1:
return 'bg-green-100 text-green-800'
case 2:
return 'bg-blue-100 text-blue-800'
case 3:
return 'bg-amber-100 text-amber-800'
default:
return 'bg-gray-100 text-gray-700'
}
}
export function commercialBadgeClass(commercial: string | null): string {
switch ((commercial || '').toLowerCase()) {
case 'allowed':
return 'bg-green-100 text-green-800'
case 'restricted':
return 'bg-amber-100 text-amber-800'
case 'prohibited':
return 'bg-red-100 text-red-800'
default:
return 'bg-gray-100 text-gray-700'
}
}
export interface UseCaseGroup {
group: string
label: string
rows: UseCaseRow[]
}
// Group use-cases by their registry group (stable order), each group's rows
// sorted by how many relevant obligations it carries (desc).
export function groupUseCases(rows: UseCaseRow[]): UseCaseGroup[] {
const order = ['document', 'security', 'cross_cutting', 'product']
const by: Record<string, UseCaseRow[]> = {}
for (const r of rows) {
;(by[r.group] ||= []).push(r)
}
const groups = order.filter((g) => by[g]?.length)
for (const g of Object.keys(by)) {
if (!order.includes(g)) groups.push(g)
}
return groups.map((g) => ({
group: g,
label: USE_CASE_GROUP_LABELS[g] || g,
rows: [...by[g]].sort((a, b) => b.atom_relevant - a.atom_relevant),
}))
}
+228
View File
@@ -0,0 +1,228 @@
import {
type UseCaseRow,
type CorpusOverview,
licenseTierBadgeClass,
commercialBadgeClass,
groupUseCases,
} from './_helpers'
const BACKEND_URL =
process.env.COMPLIANCE_BACKEND_URL || 'http://backend-compliance:8002'
export const dynamic = 'force-dynamic'
async function getData(): Promise<{
useCases: UseCaseRow[]
corpus: CorpusOverview | null
}> {
try {
const [ucRes, corpusRes] = await Promise.all([
fetch(`${BACKEND_URL}/api/compliance/v1/controls/use-cases`, {
cache: 'no-store',
}),
fetch(`${BACKEND_URL}/api/compliance/v1/controls/corpus`, {
cache: 'no-store',
}),
])
return {
useCases: ucRes.ok ? await ucRes.json() : [],
corpus: corpusRes.ok ? await corpusRes.json() : null,
}
} catch {
return { useCases: [], corpus: null }
}
}
function Stat({ label, value }: { label: string; value: string | number }) {
return (
<div className="rounded-lg border border-gray-200 bg-white px-4 py-3">
<div className="text-2xl font-semibold text-gray-900">{value}</div>
<div className="text-xs text-gray-500">{label}</div>
</div>
)
}
export default async function CoveragePage() {
const { useCases, corpus } = await getData()
const groups = groupUseCases(useCases)
const totalRelevant = useCases.reduce((s, u) => s + u.atom_relevant, 0)
const totalAtoms = useCases.reduce((s, u) => s + u.atom_total, 0)
return (
<div className="mx-auto max-w-7xl space-y-10 p-6">
<header className="space-y-1">
<h1 className="text-2xl font-bold text-gray-900">
Compliance-Abdeckung
</h1>
<p className="text-sm text-gray-600">
Alle ableitbaren Use Cases und alle Quell-Dokumente im Korpus inkl.
Lizenz damit kein Thema und keine Quelle vergessen wird.
</p>
</header>
<section className="grid grid-cols-2 gap-3 sm:grid-cols-4">
<Stat label="Use Cases" value={useCases.length} />
<Stat label="Pflichten (relevant)" value={totalRelevant.toLocaleString('de-DE')} />
<Stat label="klassifizierte Atome" value={totalAtoms.toLocaleString('de-DE')} />
<Stat label="Quell-Dokumente" value={corpus?.totals.documents ?? 0} />
</section>
{corpus?.license_summary?.length ? (
<section className="space-y-2">
<h2 className="text-lg font-semibold text-gray-900">Lizenz-Verteilung</h2>
<div className="flex flex-wrap gap-3">
{corpus.license_summary.map((l) => (
<div
key={String(l.license_rule)}
className="flex items-center gap-2 rounded-lg border border-gray-200 bg-white px-4 py-2"
>
<span
className={`rounded px-2 py-0.5 text-xs font-medium ${licenseTierBadgeClass(l.license_rule)}`}
>
Tier {l.license_rule ?? '?'}
</span>
<span className="text-sm text-gray-700">{l.label}</span>
<span className="text-sm font-semibold text-gray-900">
{l.atom_count.toLocaleString('de-DE')}
</span>
</div>
))}
</div>
</section>
) : null}
<section className="space-y-4">
<h2 className="text-lg font-semibold text-gray-900">Use Cases</h2>
{groups.map((g) => (
<div key={g.group} className="space-y-1">
<h3 className="text-sm font-semibold uppercase tracking-wide text-gray-500">
{g.label} ({g.rows.length})
</h3>
<div className="overflow-hidden rounded-lg border border-gray-200">
<table className="min-w-full divide-y divide-gray-200 text-sm">
<thead className="bg-gray-50 text-left text-xs uppercase text-gray-500">
<tr>
<th className="px-4 py-2">Use Case</th>
<th className="px-4 py-2">Key</th>
<th className="px-4 py-2 text-right">relevant</th>
<th className="px-4 py-2 text-right">klassifiziert</th>
<th className="px-4 py-2">Quellen</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-100 bg-white">
{g.rows.map((u) => (
<tr key={u.key} className={u.atom_relevant === 0 ? 'text-gray-400' : ''}>
<td className="px-4 py-2 font-medium text-gray-900">{u.label}</td>
<td className="px-4 py-2 font-mono text-xs text-gray-500">{u.key}</td>
<td className="px-4 py-2 text-right font-semibold">
{u.atom_relevant.toLocaleString('de-DE')}
</td>
<td className="px-4 py-2 text-right text-gray-500">
{u.atom_total.toLocaleString('de-DE')}
</td>
<td className="px-4 py-2 text-xs text-gray-500">
{u.regulations.slice(0, 4).join(', ')}
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
))}
</section>
<section className="space-y-2">
<h2 className="text-lg font-semibold text-gray-900">
Korpus-Dokumente ({corpus?.documents.length ?? 0})
</h2>
<p className="text-xs text-gray-500">
Quell-Regulierung × Lizenz-Tier × Anzahl Pflichten × gemappter Use Case.
</p>
<div className="max-h-[28rem] overflow-auto rounded-lg border border-gray-200">
<table className="min-w-full divide-y divide-gray-200 text-sm">
<thead className="sticky top-0 bg-gray-50 text-left text-xs uppercase text-gray-500">
<tr>
<th className="px-4 py-2">Dokument / Quelle</th>
<th className="px-4 py-2">Lizenz</th>
<th className="px-4 py-2 text-right">Pflichten</th>
<th className="px-4 py-2">Use Case</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-100 bg-white">
{(corpus?.documents ?? []).map((d) => (
<tr key={d.source_regulation}>
<td className="px-4 py-2 text-gray-900">{d.source_regulation}</td>
<td className="px-4 py-2">
<span
className={`rounded px-2 py-0.5 text-xs font-medium ${licenseTierBadgeClass(d.license_rule)}`}
title={d.license_tier}
>
Tier {d.license_rule ?? '?'}
</span>
</td>
<td className="px-4 py-2 text-right">{d.atom_count.toLocaleString('de-DE')}</td>
<td className="px-4 py-2 font-mono text-xs text-gray-600">
{d.use_case ?? <span className="text-amber-600"> ungemappt</span>}
</td>
</tr>
))}
</tbody>
</table>
</div>
</section>
{corpus?.license_catalog?.length ? (
<section className="space-y-2">
<h2 className="text-lg font-semibold text-gray-900">
Lizenz-Katalog ({corpus.license_catalog.length} kuratierte Quellen)
</h2>
<p className="text-xs text-gray-500">
Detaillierte Nutzungsrechte je kuratierter Quelle (kommerzielle
Nutzung, Auslieferung im Produkt).
</p>
<div className="overflow-auto rounded-lg border border-gray-200">
<table className="min-w-full divide-y divide-gray-200 text-sm">
<thead className="bg-gray-50 text-left text-xs uppercase text-gray-500">
<tr>
<th className="px-4 py-2">Quelle</th>
<th className="px-4 py-2">Herausgeber</th>
<th className="px-4 py-2">Lizenz</th>
<th className="px-4 py-2">kommerziell</th>
<th className="px-4 py-2">im Produkt</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-100 bg-white">
{corpus.license_catalog.map((c) => (
<tr key={c.source_id}>
<td className="px-4 py-2 text-gray-900">
{c.terms_url ? (
<a href={c.terms_url} target="_blank" rel="noreferrer" className="hover:underline">
{c.title}
</a>
) : (
c.title
)}
</td>
<td className="px-4 py-2 text-gray-600">{c.publisher ?? '—'}</td>
<td className="px-4 py-2 text-gray-600">{c.license_name ?? c.license_id ?? '—'}</td>
<td className="px-4 py-2">
<span
className={`rounded px-2 py-0.5 text-xs font-medium ${commercialBadgeClass(c.commercial_use)}`}
>
{c.commercial_use ?? 'unbekannt'}
</span>
</td>
<td className="px-4 py-2 text-gray-600">
{c.ship_in_product ? 'ja' : 'nein'}
</td>
</tr>
))}
</tbody>
</table>
</div>
</section>
) : null}
</div>
)
}
@@ -16,6 +16,7 @@ from sqlalchemy.orm import Session
from classroom_engine.database import get_db
from compliance.api._http_errors import translate_domain_errors
from compliance.services.corpus_overview import corpus_overview
from compliance.services.use_case_controls import UseCaseControlsService
router = APIRouter(prefix="/v1/controls", tags=["use-case-controls"])
@@ -36,6 +37,15 @@ async def list_use_cases(
return svc.list_use_cases()
@router.get("/corpus")
async def corpus(db: Session = Depends(get_db)) -> dict[str, Any]:
"""Korpus-Übersicht: Quell-Dokumente (source_regulation) mit Lizenz-Tier +
Atom-Count + gemapptem Use Case, plus den kuratierten Lizenz-Katalog
(canonical_control_sources ⋈ licenses) mit Nutzungsrechten."""
with translate_domain_errors():
return corpus_overview(db)
@router.get("/use-cases/{use_case}/controls")
async def controls_for_use_case(
use_case: str,
@@ -214,6 +214,23 @@ _USE_CASES: tuple[UseCase, ...] = (
UseCase("handelsrecht", "Handelsrecht", "document",
regulations=("HGB", "UGB", "ABGB"),
verification_methods=("document", "it_process")),
# ── Arbeits-/Gesellschafts-/Insolvenzrecht + ESG + Finanz-IT ─────
UseCase("arbeitsrecht", "Arbeitsrecht", "document",
regulations=("ArbVG", "AZG", "ArbZG", "MuSchG", "MiLoG",
"NachwG", "AngG", "ArG", "BUrlG"),
verification_methods=("document", "it_process")),
UseCase("gesellschaftsrecht", "Gesellschaftsrecht", "document",
regulations=("AktG", "GmbHG", "OR"),
verification_methods=("document", "it_process")),
UseCase("insolvenzrecht", "Insolvenzrecht", "document",
regulations=("InsO",),
verification_methods=("document", "it_process")),
UseCase("csrd", "Nachhaltigkeitsberichterstattung (CSRD)", "document",
regulations=("CSRD",),
verification_methods=("document", "it_process")),
UseCase("bafin_it", "BaFin IT-Aufsicht (VAIT/BAIT)", "security",
regulations=("VAIT", "BAIT"),
verification_methods=("it_process", "document", "network")),
)
@@ -256,6 +273,7 @@ _REGULATION_RULES: tuple[tuple[str, str], ...] = (
("medizinprodukte", "mdr"),
("(mdr)", "mdr"),
("maschinenverordnung", "maschinen"),
("machinery", "maschinen"),
("batterie", "batterie"),
("health data space", "ehds"),
("produktsicherheit", "produktsicherheit"),
@@ -304,6 +322,25 @@ _REGULATION_RULES: tuple[tuple[str, str], ...] = (
("bao", "steuerrecht"),
("standardvertragsklauseln", "avv"),
("(scc)", "avv"),
# Arbeits-/Gesellschafts-/Insolvenzrecht + ESG + Finanz-IT-Aufsicht
("arbeitsverfassungsgesetz", "arbeitsrecht"),
("arbeitszeitgesetz", "arbeitsrecht"),
("mutterschutzgesetz", "arbeitsrecht"),
("mindestlohngesetz", "arbeitsrecht"),
("nachweisgesetz", "arbeitsrecht"),
("angestelltengesetz", "arbeitsrecht"),
("bundesurlaubsgesetz", "arbeitsrecht"),
("arbeitsgesetz", "arbeitsrecht"),
("aktiengesetz", "gesellschaftsrecht"),
("gmbh", "gesellschaftsrecht"),
("obligationenrecht", "gesellschaftsrecht"),
("insolvenzordnung", "insolvenzrecht"),
("corporate sustainability", "csrd"),
("csrd", "csrd"),
("vait", "bafin_it"),
("bait", "bafin_it"),
("gobd", "steuerrecht"),
("dienstleistungs-informationspflichten", "impressum"),
# Datenschutz-Catch-alls (zuletzt)
("nist privacy framework", "dse"),
("dsgvo", "dse"),
@@ -0,0 +1,93 @@
"""Corpus + license overview — which source documents are in the corpus and
under which license / usage rights. Read-only; backs the admin coverage page so
the team can SEE every use-case and every ingested document with its license
(and not forget any). See use_case_controls for the per-topic retrieval.
"""
from __future__ import annotations
from typing import Any, Optional
from sqlalchemy import text
from sqlalchemy.orm import Session
from compliance.data.use_case_registry import use_case_for_regulation
# canonical_controls.license_rule is a coarse 3-tier flag (the detailed terms
# live in canonical_control_licenses, keyed per curated source).
_TIER: dict[int, str] = {
1: "Öffentlich / frei nutzbar (Public Domain, EU-Recht)",
2: "Offen mit Attribution (CC-BY / CC-BY-SA)",
3: "Eigenformulierung / eingeschränkt",
}
_LIVE = ("cc.decomposition_method = 'pass0b' "
"AND cc.release_state NOT IN ('deprecated', 'duplicate', 'rejected')")
def license_tier_label(rule: Optional[int]) -> str:
"""Human label for the coarse license_rule tier. Pure → unit-testable."""
return _TIER.get(rule or 0, "unbekannt")
def corpus_overview(db: Session) -> dict[str, Any]:
"""Three views for the coverage page: (1) atom counts per license tier,
(2) every source document (source_regulation) with tier + count + mapped
use-case, (3) the curated license catalog with detailed usage rights."""
summary = [
{
"license_rule": int(r[0]) if r[0] is not None else None,
"label": license_tier_label(r[0]),
"atom_count": int(r[1]),
}
for r in db.execute(text(
f"SELECT cc.license_rule, count(*) FROM canonical_controls cc "
f"WHERE {_LIVE} GROUP BY cc.license_rule ORDER BY cc.license_rule"
)).fetchall()
]
documents = [
{
"source_regulation": r.src,
"license_rule": int(r.lic) if r.lic is not None else None,
"license_tier": license_tier_label(r.lic),
"atom_count": int(r.n),
"use_case": use_case_for_regulation(r.src),
}
for r in db.execute(text(
f"SELECT cpl.source_regulation AS src, max(cc.license_rule) AS lic, "
f"count(DISTINCT cc.id) AS n FROM canonical_controls cc "
f"JOIN control_parent_links cpl ON cpl.control_uuid = cc.id "
f"WHERE {_LIVE} AND coalesce(cpl.source_regulation, '') <> '' "
f"GROUP BY cpl.source_regulation ORDER BY n DESC"
)).fetchall()
]
catalog: list[dict[str, Any]] = []
if db.execute(text(
"SELECT to_regclass('compliance.canonical_control_sources')"
)).scalar() is not None:
catalog = [
{
"source_id": r.source_id, "title": r.title,
"publisher": r.publisher, "url": r.url, "version": r.version_label,
"license_id": r.license_id, "license_name": r.license_name,
"commercial_use": r.commercial_use,
"ship_in_product": r.allowed_ship_in_product,
"terms_url": r.terms_url,
}
for r in db.execute(text(
"SELECT s.source_id, s.title, s.publisher, s.url, s.version_label, "
"s.license_id, s.allowed_ship_in_product, l.name AS license_name, "
"l.commercial_use, l.terms_url "
"FROM canonical_control_sources s "
"LEFT JOIN canonical_control_licenses l ON l.license_id = s.license_id "
"ORDER BY s.publisher NULLS LAST, s.title"
)).fetchall()
]
return {
"license_summary": summary,
"documents": documents,
"license_catalog": catalog,
"totals": {"documents": len(documents), "catalog_sources": len(catalog)},
}
@@ -98,7 +98,9 @@ class UseCaseControlsService:
self.db = db
def list_use_cases(self) -> list[dict[str, Any]]:
"""Registry use-cases with their live mapped-control counts."""
"""Registry use-cases with live counts — atom-grain (Haiku classification)
plus the legacy master seed. Backs the coverage overview so every topic is
visible with how many obligations it actually carries."""
counts = {
row[0]: int(row[1])
for row in self.db.execute(text(
@@ -106,6 +108,17 @@ class UseCaseControlsService:
"GROUP BY use_case"
)).fetchall()
}
atom: dict[str, tuple[int, int]] = {}
if self.db.execute(text(
"SELECT to_regclass('compliance.atom_classification')"
)).scalar() is not None:
atom = {
row[0]: (int(row[1]), int(row[2]))
for row in self.db.execute(text(
"SELECT use_case, count(*), count(*) FILTER (WHERE relevant) "
"FROM atom_classification GROUP BY use_case"
)).fetchall()
}
out = [
{
"key": uc.key,
@@ -114,10 +127,13 @@ class UseCaseControlsService:
"regulations": list(uc.regulations),
"verification_methods": list(uc.verification_methods),
"mapped_controls": counts.get(uc.key, 0),
"atom_total": atom.get(uc.key, (0, 0))[0],
"atom_relevant": atom.get(uc.key, (0, 0))[1],
}
for uc in REGISTRY.values() if uc.enabled
]
out.sort(key=lambda x: x["mapped_controls"], reverse=True)
out.sort(key=lambda x: (x["atom_relevant"], x["mapped_controls"]),
reverse=True)
return out
def controls_for_use_case(
@@ -0,0 +1,17 @@
"""Tests fuer die Korpus-/Lizenz-Uebersicht (#74)."""
from __future__ import annotations
from compliance.services.corpus_overview import license_tier_label
def test_license_tier_labels():
assert "frei nutzbar" in license_tier_label(1)
assert "Attribution" in license_tier_label(2)
assert "Eigenformulierung" in license_tier_label(3)
def test_license_tier_label_unknown_safe():
assert license_tier_label(None) == "unbekannt"
assert license_tier_label(0) == "unbekannt"
assert license_tier_label(99) == "unbekannt"
@@ -132,6 +132,28 @@ def test_regulation_mapper_known():
assert reg.use_case_for_regulation(reg_str) == expected, reg_str
def test_regulation_mapper_new_domains():
# 2026-06-14: zuvor ungemappte Quell-Gesetze -> neue Use Cases + Fixes.
cases = {
"Arbeitsverfassungsgesetz (ArbVG)": "arbeitsrecht",
"Arbeitszeitgesetz (AZG)": "arbeitsrecht",
"Mutterschutzgesetz (MuSchG)": "arbeitsrecht",
"Mindestlohngesetz (MiLoG)": "arbeitsrecht",
"Arbeitsgesetz (ArG)": "arbeitsrecht",
"Aktiengesetz (AktG)": "gesellschaftsrecht",
"GmbH-Gesetz (GmbHG)": "gesellschaftsrecht",
"Obligationenrecht (OR)": "gesellschaftsrecht",
"Insolvenzordnung (InsO)": "insolvenzrecht",
"Corporate Sustainability Reporting Directive (CSRD)": "csrd",
"VAIT (BaFin 2022)": "bafin_it",
"BAIT (BaFin 2024)": "bafin_it",
"EU Machinery Guide 2006/42": "maschinen",
"GoBD (BMF-Schreiben 2025)": "steuerrecht",
}
for reg_str, expected in cases.items():
assert reg.use_case_for_regulation(reg_str) == expected, reg_str
def test_regulation_mapper_impressum_misroutes_fixed():
# Phase A: Telekom-/Datenschutz-/Gewerbe-Gesetze duerfen NICHT mehr als
# Impressum durchgehen (Korpus enthaelt kein echtes Impressumsrecht ausser