Files
breakpilot-compliance/backend-compliance/compliance/reasoning/implementation_engine.py
T
Benjamin Admin 5e5002c883 refactor(reasoning): enforce ClaimCoverage (Welt 1) vs ComplianceStatus (Welt 2) boundary [F1]
Architecture-validation finding: the implementation mode produced compliance-
flavored output ("teilweise erfüllt", "covered") from a mere customer claim,
blurring the line to the Execution layer. This is a design decision, not a text
fix — the reasoning layer judges only the customer's STATEMENT, never conformity.

- CoverageStatus -> ClaimCoverage; values are claim-relative + carry "potential":
  potentially_addresses / partially_addresses / does_not_address /
  insufficient_information.
- ImplementationAssessment -> ClaimObligationMapping (coverage_status ->
  claim_coverage); ImplementationResponse -> ImplementationReasoningResponse
  (assessments -> mappings, + explicit `disclaimer`); request renamed; engine
  entry assess_implementation -> reason_implementation_claim.
- Endpoint /reasoning/implementation-assessment -> /reasoning/implementation-reasoning.
- Summary/explanations reworded: "adressiert wahrscheinlich N Pflichten … für
  eine Bewertung der tatsächlichen Umsetzung sind Nachweise erforderlich (keine
  Konformitätsaussage)". No "erfüllt"/"abgedeckt" leaks.
- New guard test asserts no compliance verdict leaks (no "erfüllt"; disclaimer
  separates ClaimCoverage from ComplianceStatus). 23 tests green, mypy clean.

Discovery (scope/obligations) was already structurally claim-free and unaffected.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-26 00:37:57 +02:00

159 lines
7.0 KiB
Python

"""Implementation reasoning (spec Modus 3) — Welt 1 only.
Maps a free-text claim ("Wir haben SBOMs und machen Updates, wenn Kunden Fehler
melden.") onto the product's applicable obligations and reports, per obligation,
whether the *claim* potentially/partially/does-not address it — plus the
evidence that WOULD be needed to prove real implementation.
This is NOT a conformity verdict. It judges the customer's statement, never
whether the obligation is met. The real verdict (ComplianceStatus: erfüllt/
offen/unklar from verified evidence) lives in the Compliance Execution Graph.
The four reasoning layers: claim -> interpretation (capabilities/topics on the
claim) -> potential obligation coverage (`claim_coverage`) -> evidence required.
"""
from __future__ import annotations
from typing import Dict, List
from .claim_normalizer import normalize_claim
from .enums import ClaimCoverage, Confidence
from .obligation_engine import derive_obligations
from .schemas import (
ClaimObligationMapping,
CustomerImplementationClaim,
ImplementationReasoningResponse,
ProductProfile,
)
from .taxonomy_claims import topics_for
DISCLAIMER = (
"Diese Auswertung interpretiert ausschließlich die Kundenaussage (ClaimCoverage, Welt 1). "
"Sie ist KEINE Konformitätsaussage — der tatsächliche Compliance-Status (ComplianceStatus, "
"Welt 2) ergibt sich erst aus geprüften Nachweisen im Compliance Execution Graph."
)
# 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]) -> ClaimCoverage:
if not required:
return ClaimCoverage.INSUFFICIENT_INFORMATION
req, have = set(required), set(claimed)
hit = req & have
if not hit:
return ClaimCoverage.DOES_NOT_ADDRESS
if "absent" in qualifiers or "planned" in qualifiers:
return ClaimCoverage.DOES_NOT_ADDRESS
if "reactive" in qualifiers and hit & {"secure_updates", "vulnerability_management"}:
return ClaimCoverage.PARTIALLY_ADDRESSES
if req <= have:
return ClaimCoverage.POTENTIALLY_ADDRESSES
return ClaimCoverage.PARTIALLY_ADDRESSES
def reason_implementation_claim(
profile: ProductProfile, customer_claim: str
) -> ImplementationReasoningResponse:
claim = normalize_claim(customer_claim)
obligations = derive_obligations(profile).applicable_obligations
claimed = claim.claimed_capability
claim_topics = set(claim.related_topics) | set(claimed)
mappings: List[ClaimObligationMapping] = []
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 reason about it
coverage = _coverage(required_caps, claimed, claim.qualifiers)
missing = [] if coverage == ClaimCoverage.POTENTIALLY_ADDRESSES else _missing_for(required_caps)
if coverage != ClaimCoverage.POTENTIALLY_ADDRESSES:
for ev in ob.required_evidence:
if ev not in missing_evidence:
missing_evidence.append(ev)
mappings.append(
ClaimObligationMapping(
claim_id=claim.claim_id,
obligation_id=ob.obligation_id,
claim_coverage=coverage,
missing_elements=missing,
required_evidence=ob.required_evidence,
explanation=_explain(coverage, ob.title, claim.qualifiers),
confidence=Confidence.MEDIUM,
)
)
return ImplementationReasoningResponse(
claim=claim,
mappings=mappings,
missing_evidence=missing_evidence,
summary=_summary(claim, mappings),
disclaimer=DISCLAIMER,
)
def _explain(coverage: ClaimCoverage, title: str, qualifiers: List[str]) -> str:
if coverage == ClaimCoverage.POTENTIALLY_ADDRESSES:
return "Die Aussage adressiert die Pflicht '%s' wahrscheinlich vollständig — Nachweise erforderlich." % title
if coverage == ClaimCoverage.PARTIALLY_ADDRESSES:
extra = " Der beschriebene Prozess wirkt reaktiv." if "reactive" in qualifiers else ""
return "Die Aussage adressiert die Pflicht '%s' nur teilweise.%s" % (title, extra)
if coverage == ClaimCoverage.DOES_NOT_ADDRESS:
return "Die Aussage adressiert die Pflicht '%s' nicht." % title
return "Zur Pflicht '%s' liegen zu wenige Angaben für eine Einordnung vor." % title
def _summary(claim: CustomerImplementationClaim, mappings: List[ClaimObligationMapping]) -> str:
if not claim.claimed_capability:
return "Die Aussage ist zu unspezifisch — bitte konkretisieren, was umgesetzt wurde."
full = sum(1 for m in mappings if m.claim_coverage == ClaimCoverage.POTENTIALLY_ADDRESSES)
partial = sum(1 for m in mappings if m.claim_coverage == ClaimCoverage.PARTIALLY_ADDRESSES)
none = sum(1 for m in mappings if m.claim_coverage == ClaimCoverage.DOES_NOT_ADDRESS)
return (
"Die beschriebene Maßnahme adressiert wahrscheinlich %d Pflicht(en) vollständig und %d "
"teilweise; %d werden nicht berührt. Für eine Bewertung der tatsächlichen Umsetzung sind "
"Nachweise erforderlich (keine Konformitätsaussage)." % (full, partial, none)
)