feat(cra): Befund-Detail aufgeräumt + Rechts-Anker (source_article) sichtbar

Frontend (CRA/Cyber-Tab):
- Erklär-Zwischensätze je Ebene (Befund -> CRA-Anforderung -> Best-Practice-
  Standard -> Maßnahmen) + "So liest du einen Befund"-Legende.
- Kuratierte M-Maßnahmen und atom-grain "Regulatorische Breite" in EINE Sektion
  "Maßnahmen (wählbar)" zusammengeführt (statt zwei konkurrierender Listen).
- Standalone "Empfohlene Maßnahmen (Sollzustand)" entfernt (jetzt je Befund).

Backend:
- Atom-Controls-Query liefert jetzt cpl.source_article (Artikel/Anhang/Erwägungs-
  grund-Anker) zusätzlich zu source_regulation; via LATERAL-Join.
- enrich_findings_with_breadth trägt source_article in regulatory_breadth.
- Daten waren schon ingestiert (682/691 CRA-Atome haben source_article) — wurden
  nur nicht selektiert/angezeigt.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Benjamin Admin
2026-06-14 23:14:53 +02:00
parent e5ea2ee7dd
commit 828230746e
5 changed files with 116 additions and 65 deletions
@@ -76,6 +76,7 @@ def enrich_findings_with_breadth(mapped: list, db, per_use_case: int = 3) -> Non
cache[key] = [
{"control_id": c.get("control_id"), "title": c.get("title"),
"source_regulation": c.get("source_regulation"),
"source_article": c.get("source_article"),
"severity": c.get("severity"), "use_case": uc}
for c in res.get("controls", [])
]
@@ -78,10 +78,14 @@ _LIST_SQL = text("""
_ATOM_LIST_SQL = text("""
SELECT ac.control_uuid, ac.sub_topic, ac.canonical_obligation,
cc.control_id, cc.title, cc.objective, cc.severity,
(SELECT cpl.source_regulation FROM control_parent_links cpl
WHERE cpl.control_uuid = ac.control_uuid LIMIT 1) AS source_regulation
cpl.source_regulation, cpl.source_article
FROM atom_classification ac
JOIN canonical_controls cc ON cc.id = ac.control_uuid
LEFT JOIN LATERAL (
SELECT cpl.source_regulation, cpl.source_article
FROM control_parent_links cpl
WHERE cpl.control_uuid = ac.control_uuid LIMIT 1
) cpl ON true
WHERE ac.use_case = :uc AND ac.relevant = true
AND (:sub IS NULL OR ac.sub_topic = :sub)
ORDER BY ac.sub_topic NULLS LAST,
@@ -228,6 +232,7 @@ class UseCaseControlsService:
"sub_topic": r.sub_topic,
"canonical_obligation": r.canonical_obligation,
"source_regulation": r.source_regulation,
"source_article": r.source_article,
}
for r in rows
]
@@ -1,5 +1,9 @@
"""Pin the CRA-AI -> network_security sub_topic map (DB enrichment verified live)."""
from compliance.services.cra_use_case_controls import subtopic_for
from compliance.services import cra_use_case_controls
from compliance.services.cra_use_case_controls import (
enrich_findings_with_breadth,
subtopic_for,
)
from compliance.api.cra_annex_i_data import ANNEX_I_REQUIREMENTS
# Exact atom-grain sub_topic keys (verified against the live atom_classification).
@@ -14,3 +18,32 @@ def test_every_requirement_maps_to_a_valid_subtopic():
for req in ANNEX_I_REQUIREMENTS:
st = subtopic_for(req["req_id"])
assert st in _VALID, "{} -> {}".format(req["req_id"], st)
class _FakeControlsService:
"""Stands in for UseCaseControlsService: returns one atom control per call,
carrying the legal anchor (source_article) the real atom query now selects."""
def __init__(self, db):
pass
def controls_for_use_case(self, use_case, sub_topic=None, limit=3):
return {"controls": [{
"control_id": "AI-{}-{}".format(use_case, sub_topic),
"title": "Test obligation",
"source_regulation": "Cyber Resilience Act (CRA)",
"source_article": "Artikel 13",
"severity": "high",
}]}
def test_breadth_carries_source_article(monkeypatch):
monkeypatch.setattr(
cra_use_case_controls, "UseCaseControlsService", _FakeControlsService,
)
mapped = [{"primary_requirement": "CRA-AI-8"}] # -> authentication sub_topic
enrich_findings_with_breadth(mapped, db=None)
breadth = mapped[0]["regulatory_breadth"]
assert breadth, "expected breadth controls"
assert all("source_article" in c for c in breadth)
assert any(c["source_article"] == "Artikel 13" for c in breadth)