feat(interpretation-in-map): judge a customer interpretation within the map (step 5)

Thin adapter — it judges the customer's reading WITHIN the already-built
RegulatoryMap, it does not assess abstract legal questions and it is not RCI.

- Reuses the existing assess_interpretation (no new legal reasoning); the 6
  verdicts (plausible/too_narrow/too_broad/partially_correct/unsupported/uncertain)
  pass through unchanged.
- Restricts affected_regulations/affected_obligations to those present in the map
  (intersection); links to the map's uncertain regulations.
- Touched unsupported domains (wastewater/chemicals/...) are reported as
  future_corpus_domains (future_corpus_needed) — never pseudo-evaluated.
- Customer-readable explanation ("Ihre Interpretation ist wahrscheinlich zu eng. …
  Betroffen in Ihrer Map: CRA.").
- POST /reasoning/interpretation-in-map (renders the map, then interprets).
- 7 tests; 63 green (existing reasoning MVP stays green), mypy clean, LOC ok.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Benjamin Admin
2026-06-26 10:58:00 +02:00
parent 9312ad18ef
commit 50ae9e94d1
5 changed files with 297 additions and 0 deletions
@@ -0,0 +1,141 @@
"""Tests for Interpretation-in-Map (step 5).
Acceptance: a customer interpretation is judged against the existing map, using
only assess_interpretation; affected regulations/obligations are referenced from
the map; unsupported domains (wastewater/chemicals) are flagged
future_corpus_needed, not pseudo-evaluated; output is customer-readable.
"""
from __future__ import annotations
import pytest
from fastapi import FastAPI
from fastapi.testclient import TestClient
from compliance.interpretation_map import interpret_in_map
from compliance.profile.canonical import (
CanonicalLifecyclePhase,
CanonicalProductRegulatoryProfile,
CanonicalProductType,
EconomicOperatorRole,
EnvironmentalImpact,
)
from compliance.reasoning.enums import InterpretationVerdict
from compliance.reasoning.interpretation_engine import assess_interpretation
from compliance.regulatory_map import render_regulatory_map
def ready_profile(**ov) -> CanonicalProductRegulatoryProfile:
base = dict(
name="Industriespülmaschine",
product_type=CanonicalProductType.MACHINERY,
markets=["EU", "DE"],
economic_operator_role=EconomicOperatorRole.MANUFACTURER,
lifecycle_phase=CanonicalLifecyclePhase.PLACING_ON_MARKET,
is_machine=True,
is_component=False,
has_software_updates=True,
has_embedded_software=True,
has_remote_access=True,
technologies=["cloud", "ota_updates"],
)
base.update(ov)
return CanonicalProductRegulatoryProfile(**base)
def _map(**ov):
return render_regulatory_map(ready_profile(**ov))
# 1 + 2. evaluated against the map, using ONLY assess_interpretation.
def test_uses_assess_interpretation_verdict():
text = "Wir glauben, der CRA gilt nur für neue Produkte."
result = interpret_in_map(_map(), text)
assert result.assessment == assess_interpretation(text).assessment == InterpretationVerdict.TOO_NARROW
assert "CRA" in result.affected_regulations # CRA is in the map
assert result.in_scope_of_map is True
# 3. the six verdict values pass through unchanged.
def test_verdict_values():
m = _map()
assert interpret_in_map(m, "CRA gilt nur für neue Produkte.").assessment == InterpretationVerdict.TOO_NARROW
assert interpret_in_map(m, "Open Source ist ausgenommen, also betrifft uns der CRA nicht.").assessment == InterpretationVerdict.PARTIALLY_CORRECT
assert interpret_in_map(m, "Der Mond beeinflusst unsere Updatezyklen.").assessment == InterpretationVerdict.UNCERTAIN
# 4. affected regulations/obligations are referenced FROM the map.
def test_affected_refs_from_map():
m = _map()
result = interpret_in_map(m, "Eine SBOM reicht, dann sind wir fertig.")
map_ob_ids = {o.obligation_id for v in m.applicable_regulations for o in v.obligations}
map_reg_ids = {v.regulation_id for v in m.applicable_regulations} | {v.regulation_id for v in m.uncertain_regulations}
assert "sbom_creation" in result.affected_obligations
assert set(result.affected_obligations) <= map_ob_ids
assert set(result.affected_regulations) <= map_reg_ids
# 5. environmental aspects are NOT pseudo-evaluated.
def test_environmental_not_pseudo_evaluated():
m = _map(environmental=EnvironmentalImpact(discharges_to_wastewater=True))
result = interpret_in_map(m, "Beim Abwasser sind wir nicht betroffen, das spielt für uns keine Rolle.")
domains = {d.domain for d in result.future_corpus_domains}
assert "environment_water" in domains
assert "future_corpus_needed" in result.explanation
# 6. output is customer-readable.
def test_customer_readable():
result = interpret_in_map(_map(), "Der CRA gilt nur für neue Produkte.")
assert "zu eng" in result.explanation
assert result.explanation.startswith("Ihre Interpretation ist wahrscheinlich")
# affected refs never leave the map (no abstract legal questions).
def test_affected_regs_never_outside_map():
m = _map()
map_reg_ids = (
{v.regulation_id for v in m.applicable_regulations}
| {v.regulation_id for v in m.uncertain_regulations}
| {v.regulation_id for v in m.excluded_regulations}
)
for text in ["CRA gilt nur für neue Produkte.", "Ohne Funkmodul keine Cyber-Pflichten.", "SBOM reicht."]:
result = interpret_in_map(m, text)
assert set(result.affected_regulations) <= map_reg_ids
# endpoint smoke.
@pytest.fixture(scope="module")
def client():
from compliance.api.reasoning_routes import router
app = FastAPI()
app.include_router(router)
return TestClient(app)
def test_endpoint_interpretation_in_map(client):
r = client.post(
"/reasoning/interpretation-in-map",
json={
"product_profile": {
"name": "M",
"product_type": "machinery",
"markets": ["EU"],
"economic_operator_role": "manufacturer",
"lifecycle_phase": "placing_on_market",
"is_machine": True,
"is_component": False,
"has_software_updates": True,
"has_embedded_software": True,
"has_remote_access": True,
"technologies": ["cloud"],
},
"customer_interpretation": "Der CRA gilt nur für neue Produkte.",
},
)
assert r.status_code == 200
body = r.json()
assert body["assessment"] == "too_narrow"
assert "CRA" in body["affected_regulations"]
assert "zu eng" in body["explanation"]