feat(reasoning): Regulatory Reasoning Engine MVP (scope/obligations/implementation/interpretation)
Deterministic reasoning layer ON TOP of the Legal Knowledge Graph (obligation
registry) and the Compliance Execution Graph (control mapping/evidence). Answers
which regulations apply to a concrete product, which obligations follow, whether
the customer's implementation covers them, and whether a customer interpretation
is too narrow/broad/plausible.
- ProductProfile with tri-state facts (Optional[bool]=None => uncertain, never
false security); safe predicate evaluator (no eval).
- 6 regulation triggers (CRA/MaschinenVO/RED/EMV/DataAct/NIS2) with missing-fact
prompts; 24 obligation scope rules.
- CRA obligation_ids RE-USED verbatim from the registry (93 ids) — never re-minted
(control_uuid trap); Machine/Data-Act flagged proposed=True.
- required_evidence constrained to the framework-agnostic shared evidence catalog;
capabilities echo the planned Obligation->Capability layer.
- Overlap groups (CRA<->MaschinenVO cyber-safety) + evidence-for-multiple (USP).
- 4 endpoints POST /reasoning/{scope,obligations,implementation-assessment,
interpretation-assessment}; thin handlers, registered in api/__init__.py.
- 22 tests (5 machine-builder scenarios + 10 acceptance questions). No DB
migration, no RAG, no new controls.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,142 @@
|
||||
"""Implementation reasoning engine (spec Modus 3).
|
||||
|
||||
Given a free-text claim ("Wir haben SBOMs und machen Updates, wenn Kunden Fehler
|
||||
melden.") it maps the claimed capabilities onto the product's applicable
|
||||
obligations and reports, per obligation, whether it is covered, partially
|
||||
covered or not covered — plus the evidence that would close the gap.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Dict, List
|
||||
|
||||
from .claim_normalizer import normalize_claim
|
||||
from .enums import Confidence, CoverageStatus
|
||||
from .obligation_engine import derive_obligations
|
||||
from .schemas import (
|
||||
CustomerImplementationClaim,
|
||||
ImplementationAssessment,
|
||||
ImplementationResponse,
|
||||
ProductProfile,
|
||||
)
|
||||
from .taxonomy_claims import topics_for
|
||||
|
||||
# Typical sub-elements a capability still misses when only partially claimed.
|
||||
STANDARD_GAPS: Dict[str, List[str]] = {
|
||||
"software_bill_of_materials": [
|
||||
"Vulnerability-Monitoring der Komponenten",
|
||||
"Bewertung betroffener Komponenten",
|
||||
"Lieferantenprozess",
|
||||
],
|
||||
"secure_updates": [
|
||||
"aktive Schwachstellenüberwachung",
|
||||
"Patch-Bewertung",
|
||||
"Fristen und Verantwortlichkeiten",
|
||||
"Nachweis der Updatefähigkeit",
|
||||
],
|
||||
"vulnerability_management": [
|
||||
"definierter Vulnerability-Handling-Prozess",
|
||||
"Priorisierung und Fristen",
|
||||
],
|
||||
"authentication": ["MFA für privilegierte Zugänge", "keine Standard-Zugangsdaten"],
|
||||
"security_logging": ["Schutz der Logs vor Manipulation", "Monitoring/Alerting"],
|
||||
"software_integrity": ["Signierung der Updates", "Verifikation der Update-Signatur"],
|
||||
"secure_by_default": ["Härtung der Auslieferungskonfiguration", "Minimierung der Angriffsfläche"],
|
||||
"secure_communication": ["verschlüsselte Übertragung", "Integritätsschutz der Verbindung"],
|
||||
"risk_assessment": ["dokumentierte Risikobewertung", "Aufnahme in die technische Doku"],
|
||||
"technical_documentation": ["vollständige technische Unterlagen", "Aktualisierung über den Lebenszyklus"],
|
||||
}
|
||||
|
||||
|
||||
def _missing_for(capabilities: List[str]) -> List[str]:
|
||||
out: List[str] = []
|
||||
for cap in capabilities:
|
||||
for gap in STANDARD_GAPS.get(cap, []):
|
||||
if gap not in out:
|
||||
out.append(gap)
|
||||
return out
|
||||
|
||||
|
||||
def _coverage(required: List[str], claimed: List[str], qualifiers: List[str]) -> CoverageStatus:
|
||||
req, have = set(required), set(claimed)
|
||||
hit = req & have
|
||||
if not hit:
|
||||
return CoverageStatus.NOT_COVERED
|
||||
if "absent" in qualifiers or "planned" in qualifiers:
|
||||
return CoverageStatus.NOT_COVERED
|
||||
if "reactive" in qualifiers and hit & {"secure_updates", "vulnerability_management"}:
|
||||
return CoverageStatus.PARTIALLY_COVERED
|
||||
if req <= have:
|
||||
return CoverageStatus.COVERED
|
||||
return CoverageStatus.PARTIALLY_COVERED
|
||||
|
||||
|
||||
def assess_implementation(profile: ProductProfile, customer_claim: str) -> ImplementationResponse:
|
||||
claim = normalize_claim(customer_claim)
|
||||
obligations = derive_obligations(profile).applicable_obligations
|
||||
claimed = claim.claimed_capability
|
||||
claim_topics = set(claim.related_topics) | set(claimed)
|
||||
|
||||
assessments: List[ImplementationAssessment] = []
|
||||
missing_evidence: List[str] = []
|
||||
|
||||
for ob in obligations:
|
||||
from .rules_obligations import obligation_rule
|
||||
|
||||
rule = obligation_rule(ob.obligation_id)
|
||||
required_caps = rule.required_capabilities if rule else []
|
||||
ob_topics = set(topics_for(required_caps)) | set(required_caps)
|
||||
directly_claimed = bool(set(required_caps) & set(claimed))
|
||||
related = bool(ob_topics & claim_topics)
|
||||
if not directly_claimed and not related:
|
||||
continue # unrelated to the claim -> don't assess
|
||||
|
||||
status = _coverage(required_caps, claimed, claim.qualifiers)
|
||||
missing = [] if status == CoverageStatus.COVERED else _missing_for(required_caps)
|
||||
explanation = _explain(status, ob.title, claim.qualifiers)
|
||||
if status != CoverageStatus.COVERED:
|
||||
for ev in ob.required_evidence:
|
||||
if ev not in missing_evidence:
|
||||
missing_evidence.append(ev)
|
||||
assessments.append(
|
||||
ImplementationAssessment(
|
||||
claim_id=claim.claim_id,
|
||||
obligation_id=ob.obligation_id,
|
||||
coverage_status=status,
|
||||
missing_elements=missing,
|
||||
required_evidence=ob.required_evidence,
|
||||
explanation=explanation,
|
||||
confidence=Confidence.MEDIUM,
|
||||
)
|
||||
)
|
||||
|
||||
return ImplementationResponse(
|
||||
claim=claim,
|
||||
assessments=assessments,
|
||||
missing_evidence=missing_evidence,
|
||||
summary=_summary(claim, assessments),
|
||||
)
|
||||
|
||||
|
||||
def _explain(status: CoverageStatus, title: str, qualifiers: List[str]) -> str:
|
||||
if status == CoverageStatus.COVERED:
|
||||
return "Die Pflicht '%s' wird durch die beschriebene Umsetzung plausibel abgedeckt." % title
|
||||
if status == CoverageStatus.PARTIALLY_COVERED:
|
||||
extra = " Der Prozess wirkt reaktiv." if "reactive" in qualifiers else ""
|
||||
return "Die Pflicht '%s' ist nur teilweise abgedeckt.%s" % (title, extra)
|
||||
return "Die Pflicht '%s' wird durch die Aussage nicht abgedeckt." % title
|
||||
|
||||
|
||||
def _summary(claim: CustomerImplementationClaim, assessments: List[ImplementationAssessment]) -> str:
|
||||
if not claim.claimed_capability:
|
||||
return "Die Aussage ist zu unspezifisch — bitte konkretisieren, was umgesetzt wurde."
|
||||
covered = sum(1 for a in assessments if a.coverage_status == CoverageStatus.COVERED)
|
||||
partial = sum(1 for a in assessments if a.coverage_status == CoverageStatus.PARTIALLY_COVERED)
|
||||
notc = sum(1 for a in assessments if a.coverage_status == CoverageStatus.NOT_COVERED)
|
||||
if notc or partial:
|
||||
head = "Teilweise erfüllt"
|
||||
elif covered:
|
||||
head = "Plausibel abgedeckt"
|
||||
else:
|
||||
head = "Nicht beurteilbar"
|
||||
return "%s: %d abgedeckt, %d teilweise, %d offen." % (head, covered, partial, notc)
|
||||
Reference in New Issue
Block a user