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>
This commit is contained in:
@@ -13,7 +13,7 @@ only (spec §14).
|
||||
from __future__ import annotations
|
||||
|
||||
from .claim_normalizer import normalize_claim
|
||||
from .implementation_engine import assess_implementation
|
||||
from .implementation_engine import reason_implementation_claim
|
||||
from .interpretation_engine import assess_interpretation
|
||||
from .obligation_engine import derive_obligations
|
||||
from .scope_engine import discover_scope
|
||||
@@ -22,6 +22,6 @@ __all__ = [
|
||||
"discover_scope",
|
||||
"derive_obligations",
|
||||
"normalize_claim",
|
||||
"assess_implementation",
|
||||
"reason_implementation_claim",
|
||||
"assess_interpretation",
|
||||
]
|
||||
|
||||
@@ -68,12 +68,19 @@ class OverlapType(str, Enum):
|
||||
DIFFERENT_SCOPE = "different_scope"
|
||||
|
||||
|
||||
class CoverageStatus(str, Enum):
|
||||
COVERED = "covered"
|
||||
PARTIALLY_COVERED = "partially_covered"
|
||||
NOT_COVERED = "not_covered"
|
||||
UNCLEAR = "unclear"
|
||||
OUT_OF_SCOPE = "out_of_scope"
|
||||
class ClaimCoverage(str, Enum):
|
||||
"""How a customer's *claim* relates to an obligation — Welt 1 (reasoning).
|
||||
|
||||
This is NOT a conformity verdict. It judges only the customer's statement,
|
||||
never whether the obligation is actually met. The real compliance verdict
|
||||
(erfüllt/offen/unklar from verified evidence) is `ComplianceStatus`, owned by
|
||||
the Compliance Execution Graph — the two must never be conflated.
|
||||
"""
|
||||
|
||||
POTENTIALLY_ADDRESSES = "potentially_addresses"
|
||||
PARTIALLY_ADDRESSES = "partially_addresses"
|
||||
DOES_NOT_ADDRESS = "does_not_address"
|
||||
INSUFFICIENT_INFORMATION = "insufficient_information"
|
||||
|
||||
|
||||
class InterpretationVerdict(str, Enum):
|
||||
|
||||
@@ -1,9 +1,15 @@
|
||||
"""Implementation reasoning engine (spec Modus 3).
|
||||
"""Implementation reasoning (spec Modus 3) — Welt 1 only.
|
||||
|
||||
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.
|
||||
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
|
||||
@@ -11,16 +17,22 @@ from __future__ import annotations
|
||||
from typing import Dict, List
|
||||
|
||||
from .claim_normalizer import normalize_claim
|
||||
from .enums import Confidence, CoverageStatus
|
||||
from .enums import ClaimCoverage, Confidence
|
||||
from .obligation_engine import derive_obligations
|
||||
from .schemas import (
|
||||
ClaimObligationMapping,
|
||||
CustomerImplementationClaim,
|
||||
ImplementationAssessment,
|
||||
ImplementationResponse,
|
||||
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": [
|
||||
@@ -57,27 +69,31 @@ def _missing_for(capabilities: List[str]) -> List[str]:
|
||||
return out
|
||||
|
||||
|
||||
def _coverage(required: List[str], claimed: List[str], qualifiers: List[str]) -> CoverageStatus:
|
||||
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 CoverageStatus.NOT_COVERED
|
||||
return ClaimCoverage.DOES_NOT_ADDRESS
|
||||
if "absent" in qualifiers or "planned" in qualifiers:
|
||||
return CoverageStatus.NOT_COVERED
|
||||
return ClaimCoverage.DOES_NOT_ADDRESS
|
||||
if "reactive" in qualifiers and hit & {"secure_updates", "vulnerability_management"}:
|
||||
return CoverageStatus.PARTIALLY_COVERED
|
||||
return ClaimCoverage.PARTIALLY_ADDRESSES
|
||||
if req <= have:
|
||||
return CoverageStatus.COVERED
|
||||
return CoverageStatus.PARTIALLY_COVERED
|
||||
return ClaimCoverage.POTENTIALLY_ADDRESSES
|
||||
return ClaimCoverage.PARTIALLY_ADDRESSES
|
||||
|
||||
|
||||
def assess_implementation(profile: ProductProfile, customer_claim: str) -> ImplementationResponse:
|
||||
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)
|
||||
|
||||
assessments: List[ImplementationAssessment] = []
|
||||
mappings: List[ClaimObligationMapping] = []
|
||||
missing_evidence: List[str] = []
|
||||
|
||||
for ob in obligations:
|
||||
@@ -89,54 +105,54 @@ def assess_implementation(profile: ProductProfile, customer_claim: str) -> Imple
|
||||
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
|
||||
continue # unrelated to the claim -> don't reason about it
|
||||
|
||||
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:
|
||||
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)
|
||||
assessments.append(
|
||||
ImplementationAssessment(
|
||||
mappings.append(
|
||||
ClaimObligationMapping(
|
||||
claim_id=claim.claim_id,
|
||||
obligation_id=ob.obligation_id,
|
||||
coverage_status=status,
|
||||
claim_coverage=coverage,
|
||||
missing_elements=missing,
|
||||
required_evidence=ob.required_evidence,
|
||||
explanation=explanation,
|
||||
explanation=_explain(coverage, ob.title, claim.qualifiers),
|
||||
confidence=Confidence.MEDIUM,
|
||||
)
|
||||
)
|
||||
|
||||
return ImplementationResponse(
|
||||
return ImplementationReasoningResponse(
|
||||
claim=claim,
|
||||
assessments=assessments,
|
||||
mappings=mappings,
|
||||
missing_evidence=missing_evidence,
|
||||
summary=_summary(claim, assessments),
|
||||
summary=_summary(claim, mappings),
|
||||
disclaimer=DISCLAIMER,
|
||||
)
|
||||
|
||||
|
||||
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 _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, assessments: List[ImplementationAssessment]) -> str:
|
||||
def _summary(claim: CustomerImplementationClaim, mappings: List[ClaimObligationMapping]) -> 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)
|
||||
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)
|
||||
)
|
||||
|
||||
@@ -15,8 +15,8 @@ from pydantic import BaseModel, Field
|
||||
from .enums import (
|
||||
ApplicabilityStatus,
|
||||
AuthorityLevel,
|
||||
ClaimCoverage,
|
||||
Confidence,
|
||||
CoverageStatus,
|
||||
InterpretationVerdict,
|
||||
ManufacturerRole,
|
||||
MarketModel,
|
||||
@@ -140,10 +140,17 @@ class CustomerImplementationClaim(BaseModel):
|
||||
evidence_refs: List[str] = Field(default_factory=list)
|
||||
|
||||
|
||||
class ImplementationAssessment(BaseModel):
|
||||
class ClaimObligationMapping(BaseModel):
|
||||
"""One row of Welt-1 reasoning: how a customer claim relates to an obligation.
|
||||
|
||||
Layers (spec / architect): claim -> interpretation (on the claim object) ->
|
||||
*potential* obligation coverage (`claim_coverage`) -> evidence required.
|
||||
Carries NO compliance verdict.
|
||||
"""
|
||||
|
||||
claim_id: str
|
||||
obligation_id: str
|
||||
coverage_status: CoverageStatus
|
||||
claim_coverage: ClaimCoverage
|
||||
missing_elements: List[str] = Field(default_factory=list)
|
||||
required_evidence: List[str] = Field(default_factory=list)
|
||||
explanation: str
|
||||
@@ -188,16 +195,19 @@ class ObligationsResponse(BaseModel):
|
||||
evidence_for_multiple: Dict[str, List[str]] = Field(default_factory=dict)
|
||||
|
||||
|
||||
class ImplementationRequest(BaseModel):
|
||||
class ImplementationReasoningRequest(BaseModel):
|
||||
product_profile: ProductProfile
|
||||
customer_claim: str
|
||||
|
||||
|
||||
class ImplementationResponse(BaseModel):
|
||||
class ImplementationReasoningResponse(BaseModel):
|
||||
claim: CustomerImplementationClaim
|
||||
assessments: List[ImplementationAssessment] = Field(default_factory=list)
|
||||
mappings: List[ClaimObligationMapping] = Field(default_factory=list)
|
||||
missing_evidence: List[str] = Field(default_factory=list)
|
||||
summary: str = ""
|
||||
# Makes the Welt-1 boundary explicit: this is advisory claim-mapping, not a
|
||||
# conformity verdict (that is ComplianceStatus in the Execution Graph).
|
||||
disclaimer: str = ""
|
||||
|
||||
|
||||
class InterpretationRequest(BaseModel):
|
||||
|
||||
Reference in New Issue
Block a user